1
"""Common python utilities for the ec2 provider"""
3
from charmhelpers.core import hookenv
10
"ec2": {"endpoint": "EC2_URL", "key": "EC2_ACCESS_KEY",
11
"secret": "EC2_SECRET_KEY"},
12
"nova": {"endpoint": "OS_AUTH_URL", "region": "OS_REGION_NAME",
13
"tenant": "OS_TENANT_NAME", "key": "OS_USERNAME",
14
"secret": "OS_PASSWORD"}}
16
REQUIRED_CONFIG_OPTIONS = {
17
"ec2": ["endpoint", "key", "secret"],
18
"nova": ["endpoint", "region", "tenant", "key", "secret"]}
21
"ec2": {"validate": "euca-describe-instances",
22
"detach": "euca-detach-volume -i %s %s"},
23
"nova": {"validate": "nova list",
24
"detach": "nova volume-detach %s %s"}}
27
class StorageServiceUtil(object):
28
"""Interact with an underlying cloud storage provider.
29
Create, attach, label and detach storage volumes using EC2 or nova APIs.
32
environment_map = None
33
required_config_options = None
36
def __init__(self, provider):
37
self.provider = provider
38
if provider not in ENVIRONMENT_MAP:
40
"ERROR: Invalid charm configuration setting for provider. "
41
"'%s' must be one of: %s" %
42
(provider, ", ".join(ENVIRONMENT_MAP.keys())),
45
self.environment_map = ENVIRONMENT_MAP[provider]
46
self.commands = PROVIDER_COMMANDS[provider]
47
self.required_config_options = REQUIRED_CONFIG_OPTIONS[provider]
49
import euca2ools.commands.euca.describevolumes as getvolumes
50
import euca2ools.commands.euca.describeinstances as getinstances
51
self.ec2_volume_class = getvolumes.DescribeVolumes
52
self.ec2_instance_class = getinstances.DescribeInstances
54
def load_environment(self):
56
Source our credentials from the configuration definitions into our
59
config_data = hookenv.config()
60
for option in self.required_config_options:
61
environment_variable = self.environment_map[option]
62
os.environ[environment_variable] = config_data[option].strip()
63
self.validate_credentials()
65
def validate_credentials(self):
67
Attempt to contact the respective ec2 or nova volume service or exit(1)
70
subprocess.check_call(self.commands["validate"], shell=True)
71
except subprocess.CalledProcessError, e:
73
"ERROR: Charm configured credentials can't access endpoint. "
78
"Validated charm configuration credentials have access to block "
81
def describe_volumes(self, volume_id=None):
82
method = getattr(self, "_%s_describe_volumes" % self.provider)
83
return method(volume_id)
85
def get_volume_id(self, volume_designation=None):
86
"""Return the ec2 or nova volume id associated with this unit
88
Optionally, C{volume_designation} can be either a volume-id or
89
volume-display-name and the matching C{volume-id} will be returned.
90
If no matching volume is return C{None}.
93
volumes = self.describe_volumes()
94
if volume_designation:
95
token = volume_designation
97
# Try to find volume label containing remote_unit name
98
token = hookenv.remote_unit()
99
for volume_id in volumes.keys():
100
volume = volumes[volume_id]
101
# Get volume by name or volume-id
102
volume_name = volume["tags"].get("volume_name", "")
103
if token == volume_id:
104
matches.append(volume_id)
105
elif token in volume_name:
106
matches.append(volume_id)
109
"Error: Multiple volumes are associated with "
110
"%s. Cannot get_volume_id." % token, hookenv.ERROR)
116
def attach_volume(self, instance_id, volume_id=None, size=None,
119
Create and attach a volume to the remote unit if none exists.
121
Attempt to attach and validate the attached volume 10
122
times. If unable to resolve the attach issues, exit in error and log
124
Log errors if the volume is in an unsupported state, and if C{in-use}
125
report it is already attached.
127
Return the device-path of the attached volume to the caller.
129
self.load_environment() # Will fail if invalid environment
130
remote_unit = hookenv.remote_unit()
131
if volume_label is None:
132
volume_label = generate_volume_label(remote_unit)
134
volume = self.describe_volumes(volume_id)
137
"Requested volume-id (%s) does not exist. Unable to "
138
"associate storage with %s" % (volume_id, remote_unit),
142
# Validate that current volume status is supported
143
while volume["status"] == "attaching":
144
hookenv.log("Volume %s still attaching. Waiting." % volume_id)
146
volume = self.describe_volumes(volume_id)
148
if volume["status"] == "in-use":
149
hookenv.log("Volume %s already attached. Done" % volume_id)
150
return volume["device"] # The device path on the instance
151
if volume["status"] != "available":
153
"Cannot attach volume. Volume has unsupported status: "
154
"%s" % volume["status"], hookenv.ERROR)
157
# No volume_id, create a new volume if one isn't already created
158
# for the principal of this JUJU_REMOTE_UNIT
159
volume_id = self.get_volume_id(volume_label)
161
create = getattr(self, "_%s_create_volume" % self.provider)
163
size = hookenv.config("default_volume_size")
164
volume_id = create(size, volume_label, instance_id)
167
hookenv.log("Attaching %s (%s)" % (volume_label, volume_id))
169
volume = self.describe_volumes(volume_id)
170
if volume["status"] == "in-use":
171
return volume["device"] # The device path on the instance
172
if volume["status"] == "available":
173
attach = getattr(self, "_%s_attach_volume" % self.provider)
174
device = attach(instance_id, volume_id)
180
"ERROR: Unable to discover device attached by "
181
"euca-attach-volume",
186
def detach_volume(self, volume_label):
187
"""Detach a volume from remote unit if present"""
188
self.load_environment() # Will fail if invalid environment
189
volume_id = self.get_volume_id(volume_label)
192
volume = self.describe_volumes(volume_id)
194
hookenv.log("Cannot find volume name to detach, done")
197
if volume["status"] == "available":
198
hookenv.log("Volume (%s) already detached. Done" % volume_id)
202
"Detaching volume (%s) from instance %s" %
203
(volume_id, volume["instance_id"]))
205
subprocess.check_call(
206
self.commands["detach"] % (volume["instance_id"], volume_id),
208
except subprocess.CalledProcessError, e:
210
"ERROR: Couldn't detach volume. %s" % str(e), hookenv.ERROR)
214
# EC2-specific methods
215
def _ec2_create_tag(self, volume_id, tag_name, tag_value=None):
216
"""Attach a tag and optional C{tag_value} to the given C{volume_id}"""
217
tag_string = tag_name
219
tag_string += "=%s" % tag_value
220
command = 'euca-create-tags %s --tag "%s"' % (volume_id, tag_string)
223
subprocess.check_call(command, shell=True)
224
except subprocess.CalledProcessError, e:
226
"ERROR: Couldn't add tags to the resource. %s" % str(e),
229
hookenv.log("Tagged (%s) to %s." % (tag_string, volume_id))
231
def _ec2_describe_instances(self, instance_id=None):
233
Use euca2ools libraries to describe instances and return a C{dict}
237
command = self.ec2_instance_class()
238
reservations = command.main()
241
"ERROR: Couldn't contact EC2 using euca-describe-instances",
244
for reservation in reservations:
245
for inst in reservation.instances:
247
"ip-address": inst.ip_address, "image-id": inst.image_id,
248
"instance-type": inst.image_id, "kernel": inst.kernel,
249
"private-dns-name": inst.private_dns_name,
250
"public-dns-name": inst.public_dns_name,
251
"reservation-id": reservation.id,
252
"state": inst.state, "tags": inst.tags,
253
"availability_zone": inst.placement}
255
if instance_id in result:
256
return result[instance_id]
260
def _ec2_describe_volumes(self, volume_id=None):
262
Use euca2ools libraries to describe volumes and return a C{dict}
266
command = self.ec2_volume_class()
267
volumes = command.main()
270
"ERROR: Couldn't contact EC2 using euca-describe-volumes",
273
for volume in volumes:
274
result[volume.id] = {
278
"snapshot_id": volume.snapshot_id,
279
"status": volume.status,
282
"availability_zone": volume.zone}
283
if "volume_name" in volume.tags:
284
result[volume.id]["volume_label"] = volume.tags["volume_name"]
286
result[volume.id]["tags"]["volume_name"] = ""
287
result[volume.id]["volume_label"] = ""
288
if volume.status == "in-use":
289
result[volume.id]["instance_id"] = (
290
volume.attach_data.instance_id)
291
result[volume.id]["device"] = volume.attach_data.device
293
if volume_id in result:
294
return result[volume_id]
298
def _ec2_create_volume(self, size, volume_label, instance_id):
299
"""Create an EC2 volume with a specific C{size} and C{volume_label}"""
300
# Volumes need to be in the same zone as the instance
302
"Creating a %sGig volume named (%s) for instance %s" %
303
(size, volume_label, instance_id))
304
instance = self._ec2_describe_instances(instance_id)
306
config_data = hookenv.config()
308
"ERROR: Could not create volume for instance %s. No instance "
309
"details discovered by euca-describe-instances. Maybe the "
310
"charm configured endpoint %s is not valid for this region." %
311
(instance_id, config_data["endpoint"]), hookenv.ERROR)
315
output = subprocess.check_output(
316
"euca-create-volume -z %s -s %s" %
317
(instance["availability_zone"], size), shell=True)
318
except subprocess.CalledProcessError, e:
319
hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
322
response_type, volume_id = output.split()[:2]
323
if response_type != "VOLUME":
325
"ERROR: Didn't get VOLUME response from euca-create-volume. "
326
"Response: %s" % output, hookenv.ERROR)
328
volume = self.describe_volumes(volume_id.strip())
331
"ERROR: Unable to find volume '%s'" % volume_id.strip(),
334
volume_id = volume["id"]
335
self._ec2_create_tag(volume_id, "volume_name", volume_label)
338
def _ec2_attach_volume(self, instance_id, volume_id):
340
Attach an EC2 C{volume_id} to the provided C{instance_id} and return
345
subprocess.check_call(
346
"euca-attach-volume -i %s -d %s %s" %
347
(instance_id, device, volume_id), shell=True)
348
except subprocess.CalledProcessError, e:
349
hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
353
# Nova-specific methods
354
def _nova_volume_show(self, volume_id):
356
Read detailed information about a C{volume_id} from nova-volume-show
357
and return a C{dict} of data we are interested in.
359
from ast import literal_eval
360
result = {"tags": {}, "instance_id": "", "device": ""}
361
command = "nova volume-show '%s'" % volume_id
363
output = subprocess.check_output(command, shell=True)
364
except subprocess.CalledProcessError, e:
366
"ERROR: Failed to get nova volume info. %s" % str(e),
369
for line in output.split("\n"):
370
if not line.strip(): # Skip empty lines
372
if "+----" in line or "Property" in line:
374
(_, key, value, _) = line.split("|")
376
value = value.strip()
377
if key in ["availability_zone", "size", "id", "snapshot_id",
380
if key == "display_name": # added for compatibility with ec2
381
result["volume_label"] = value
382
result["tags"]["volume_label"] = value
383
if key == "attachments":
384
attachments = literal_eval(value)
386
for key, value in attachments[0].items():
387
if key in ["device"]:
389
if key == "server_id":
390
result["instance_id"] = value
393
def _nova_describe_volumes(self, volume_id=None):
394
"""Create a C{dict} describing all nova volumes"""
396
command = "nova volume-list"
398
output = subprocess.check_output(command, shell=True)
399
except subprocess.CalledProcessError, e:
400
hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
402
for line in output.split("\n"):
403
if not line.strip(): # Skip empty lines
405
if "+----" in line or "ID" in line:
407
values = line.split("|")
408
if volume_id and values[1].strip() != volume_id:
410
volume = values[1].strip()
411
volume_label = values[3].strip()
412
if volume_label == "None":
414
instance_id = values[6].strip()
415
if instance_id == "None":
419
"tags": {"volume_name": volume_label},
420
"status": values[2].strip(),
421
"volume_label": volume_label,
422
"size": values[4].strip(),
423
"instance_id": instance_id}
425
result[volume].update(self._nova_volume_show(volume))
427
if volume_id in result:
428
return result[volume_id]
432
def _nova_attach_volume(self, instance_id, volume_id):
434
Attach a Nova C{volume_id} to the provided C{instance_id} and return
438
device = subprocess.check_output(
439
"nova volume-attach %s %s auto | egrep -o \"/dev/vd[b-z]\"" %
440
(instance_id, volume_id), shell=True)
441
except subprocess.CalledProcessError, e:
442
hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
445
return device.strip()
448
def _nova_create_volume(self, size, volume_label, instance_id):
449
"""Create an Nova volume with a specific C{size} and C{volume_label}"""
451
"Creating a %sGig volume named (%s) for instance %s" %
452
(size, volume_label, instance_id))
454
subprocess.check_call(
455
"nova volume-create --display-name '%s' %s" %
456
(volume_label, size), shell=True)
457
except subprocess.CalledProcessError, e:
458
hookenv.log("ERROR: %s" % str(e), hookenv.ERROR)
461
volume_id = self.get_volume_id(volume_label)
464
"ERROR: Couldn't find newly created nova volume '%s'." %
465
volume_label, hookenv.ERROR)
470
def generate_volume_label(remote_unit):
471
"""Create a volume label for the requesting remote unit"""
472
return "%s unit volume" % remote_unit