def Grow(self, size): """Grow disk to the newly specified size. Size must be less than 1024 and must be greater than the current size. >>> clc.v2.Server("WA1BTDIX01").Disks().disks[2].Grow(30).WaitUntilComplete() 0 """ if size > 1024: raise (clc.CLCException("Cannot grow disk beyond 1024GB")) if size <= self.size: raise (clc.CLCException("New size must exceed current disk size")) # TODO - Raise exception if too much total size (4TB standard, 1TB HS) disk_set = [{ 'diskId': o.id, 'sizeGB': o.size } for o in self.parent.disks if o != self] self.size = size disk_set.append({'diskId': self.id, 'sizeGB': self.size}) self.parent.server.dirty = True return (clc.v2.Requests(clc.v2.API.Call( 'PATCH', 'servers/%s/%s' % (self.parent.server.alias, self.parent.server.id), json.dumps([{ "op": "set", "member": "disks", "value": disk_set }])), alias=self.parent.server.alias))
def Create(name,template,group_id,network_id,cpu=None,memory=None,alias=None,password=None,ip_address=None, storage_type="standard",type="standard",primary_dns=None,secondary_dns=None, additional_disks=[],custom_fields=[],ttl=None,managed_os=False,description=None, source_server_password=None,cpu_autoscale_policy_id=None,anti_affinity_policy_id=None, packages=[]): """Creates a new server. https://www.centurylinkcloud.com/api-docs/v2/#servers-create-server cpu and memory are optional and if not provided we pull from the default server size values associated with the provided group_id. Set ttl as number of seconds before server is to be terminated. Must be >3600 >>> d = clc.v2.Datacenter() >>> clc.v2.Server.Create(name="api2",cpu=1,memory=1, group_id=d.Groups().Get("Default Group").id, template=d.Templates().Search("centos-6-64")[0].id, network_id=d.Networks().networks[0].id).WaitUntilComplete() 0 """ if not alias: alias = clc.v2.Account.GetAlias() if not cpu or not memory: group = clc.v2.Group(id=group_id,alias=alias) if not cpu and group.Defaults("cpu"): cpu = group.Defaults("cpu") elif not cpu: raise(clc.CLCException("No default CPU defined")) if not memory and group.Defaults("memory"): memory = group.Defaults("memory") elif not memory: raise(clc.CLCException("No default Memory defined")) if not description: description = name if type.lower() == "standard" and storage_type.lower() not in ("standard","premium"): raise(clc.CLCException("Invalid storage_type")) if type.lower() == "hyperscale" and storage_type.lower() != "hyperscale": raise(clc.CLCException("Invalid type/storage_type combo")) if ttl and ttl<=3600: raise(clc.CLCException("ttl must be greater than 3600 seconds")) if ttl: ttl = clc.v2.time_utils.SecondsToZuluTS(int(time.time())+ttl) if type.lower() == "baremetal": type = "bareMetal" # TODO - validate custom_fields as a list of dicts with an id and a value key # TODO - validate template exists # TODO - validate additional_disks as a list of dicts with a path, sizeGB, and type (partitioned,raw) keys # TODO - validate addition_disks path not in template reserved paths # TODO - validate antiaffinity policy id set only with type=hyperscale return(clc.v2.Requests(clc.v2.API.Call('POST','servers/%s' % (alias), json.dumps({'name': name, 'description': description, 'groupId': group_id, 'sourceServerId': template, 'isManagedOS': managed_os, 'primaryDNS': primary_dns, 'secondaryDNS': secondary_dns, 'networkId': network_id, 'ipAddress': ip_address, 'password': password, 'sourceServerPassword': source_server_password, 'cpu': cpu, 'cpuAutoscalePolicyId': cpu_autoscale_policy_id, 'memoryGB': memory, 'type': type, 'storageType': storage_type, 'antiAffinityPolicyId': anti_affinity_policy_id, 'customFields': custom_fields, 'additionalDisks': additional_disks, 'ttl': ttl, 'packages': packages})), alias=alias))
def SetPassword(self, password): """Request change of password. The API request requires supplying the current password. For this we issue a call to retrieve the credentials so note there will be an activity log for retrieving the credentials associated with any SetPassword entry >>> s.SetPassword("newpassword") """ # 0: {op: "set", member: "password", value: {current: " r`5Mun/vT:qZ]2?z", password: "******"}} if self.data['status'] != "active": raise (clc.CLCException( "Server must be powered on to change password")) return (clc.v2.Requests(clc.v2.API.Call( 'PATCH', 'servers/%s/%s' % (self.alias, self.id), json.dumps([{ "op": "set", "member": "password", "value": { "current": self.Credentials()['password'], "password": password } }]), session=self.session), alias=self.alias, session=self.session))
def ConvertToTemplate(self, visibility, description=None, password=None): """Converts existing server to a template. visibility is one of private or shared. >>> d = clc.v2.Datacenter() >>> clc.v2.Server(alias='BTDI',id='WA1BTDIAPI207').ConvertToTemplate("private","my template") 0 """ if visibility not in ('private', 'shared'): raise (clc.CLCException( "Invalid visibility - must be private or shared")) if not password: password = self.Credentials()['password'] if not description: description = self.description return (clc.v2.Requests(clc.v2.API.Call( 'POST', 'servers/%s/%s/convertToTemplate' % (self.alias, self.id), json.dumps({ "description": description, "visibility": visibility, "password": password }), session=self.session), alias=self.alias, session=self.session))
def RestoreSnapshot(self, name=None): """Restores an existing Hypervisor level snapshot. Supply snapshot name to restore If no snapshot name is supplied will restore the first snapshot found >>> clc.v2.Server(alias='BTDI',id='WA1BTDIKRT02').RestoreSnapshot().WaitUntilComplete() 0 """ if not len(self.data['details']['snapshots']): raise (clc.CLCException("No snapshots exist")) if name is None: name = self.GetSnapshots()[0] name_links = [ obj['links'] for obj in self.data['details']['snapshots'] if obj['name'] == name ][0] return (clc.v2.Requests(clc.v2.API.Call( 'POST', [obj['href'] for obj in name_links if obj['rel'] == 'restore'][0], session=self.session), alias=self.alias, session=self.session))
def CreateSnapshot(self, delete_existing=True, expiration_days=7): """Take a Hypervisor level snapshot retained for between 1 and 10 days (7 is default). Currently only one snapshop may exist at a time, thus will delete snapshots if one already exists before taking this snapshot. >>> clc.v2.Server("WA1BTDIAPI219").CreateSnapshot(2) <clc.APIv2.queue.Requests object at 0x10d106cd0> >>> _.WaitUntilComplete() 0 """ if len(self.data['details']['snapshots']): if delete_existing: self.DeleteSnapshot() else: raise (clc.CLCException( "Snapshot already exists cannot take another")) return (clc.v2.Requests(clc.v2.API.Call( 'POST', 'operations/%s/servers/createSnapshot' % (self.alias), { 'serverIds': self.id, 'snapshotExpirationDays': expiration_days }, session=self.session), alias=self.alias, session=self.session))
def Server(self): """Return server associated with this request. >>> d = clc.v2.Datacenter() >>> q = clc.v2.Server.Create(name="api2",cpu=1,memory=1,group_id=d.Groups().Get("Default Group").id,template=d.Templates().Search("centos-6-64")[0].id,network_id=d.Networks().networks[0].id,ttl=4000) >>> q.WaitUntilComplete() 0 >>> q.success_requests[0].Server() <clc.APIv2.server.Server object at 0x1095a8390> >>> print _ VA1BTDIAPI214 """ if self.context_key == 'newserver': server_id = clc.v2.API.Call('GET', self.context_val, session=self.session)['id'] return (clc.v2.Server(id=server_id, alias=self.alias, session=self.session)) elif self.context_key == 'server': return (clc.v2.Server(id=self.context_val, alias=self.alias, session=self.session)) else: raise (clc.CLCException("%s object not server" % self.context_key))
def WaitUntilComplete(self, poll_freq=2, timeout=None): """Poll until status is completed. If status is 'notStarted' or 'executing' continue polling. If status is 'succeeded' return Else raise exception poll_freq option is in seconds """ start_time = time.time() while not self.time_completed: status = self.Status() if status == 'executing': if not self.time_executed: self.time_executed = time.time() if clc.v2.time_utils.TimeoutExpired(start_time, timeout): raise clc.RequestTimeoutException( 'Timeout waiting for Request: {0}'.format(self.id), status) elif status == 'succeeded': self.time_completed = time.time() elif status in ("failed", "resumed" or "unknown"): # TODO - need to ID best reaction for resumed status (e.g. manual intervention) self.time_completed = time.time() raise (clc.CLCException( "%s %s execution %s" % (self.context_key, self.context_val, status))) time.sleep(poll_freq)
def __init__(self,id,alias=None,server_obj=None): """Create Server object. http://www.centurylinkcloud.com/api-docs/v2#servers-get-server #If parameters are populated then create object location. #Else if only id is supplied issue a Get Policy call # successful creation >>> clc.v2.Server("CA3BTDICNTRLM01") <clc.APIv2.server.Server object at 0x10c28fe50> >>> print _ CA3BTDICNTRLM01 # error. API returns 404 when server does not exist, we raise exception >>> clc.v2.Server(alias='BTDI',id='WA1BTDIKRT01') clc.CLCException: Server does not exist """ self.id = id self.capabilities = None self.disks = None self.public_ips = None self.dirty = False if alias: self.alias = alias else: self.alias = clc.v2.Account.GetAlias() if server_obj: self.data = server_obj else: try: self.Refresh() except clc.APIFailedResponse as e: if e.response_status_code==404: raise(clc.CLCException("Server does not exist"))
def __init__(self,alias=None,location=None,networks_lst=None): if alias: self.alias = alias else: self.alias = clc.v2.Account.GetAlias() self.networks = [] if networks_lst: for network in networks_lst: self.networks.append(Network(id=network['networkId'],alias=network['accountID'],network_obj=network)) elif location: self._Load(location) else: raise(clc.CLCException("Networks object requires location or networks_lst"))
def __init__(self, id, alias=None, network_obj=None, session=None): """Create Network object.""" self.id = id self.type = type self.dirty = False self.data = network_obj self.session = session if alias: self.alias = alias else: self.alias = clc.v2.Account.GetAlias(session=self.session) if network_obj: self.name = network_obj['name'] else: try: self.Refresh() except clc.APIFailedResponse as e: if e.response_status_code == 404: raise (clc.CLCException("Network does not exist")) else: raise (clc.CLCException( "An error occurred while creating the Network object"))
def Get(self,key): """Get group by providing name, ID, description or other unique key. If key is not unique and finds multiple matches only the first will be returned >>> clc.v2.Datacenter().Groups().Get("Default Group") <clc.APIv2.group.Group object at 0x1065e5250> """ for group in self.groups: if group.id.lower() == key.lower(): return(group) elif group.name.lower() == key.lower(): return(group) elif group.description.lower() == key.lower(): return(group) raise(clc.CLCException("Group not found")) # No Match
def Add(self, size, path=None, type="partitioned"): """Add new disk. This request will error if disk is protected and cannot be removed (e.g. a system disk) # Partitioned disk >>> clc.v2.Server("WA1BTDIX01").Disks().Add(size=20,path=None,type="raw").WaitUntilComplete() 0 # Raw disk >>> clc.v2.Server("WA1BTDIX01").Disks().Add(size=20,path=None,type="raw").WaitUntilComplete() 0 """ if type == "partitioned" and not path: raise (clc.CLCException("Must specify path to mount new disk")) # TODO - Raise exception if too many disks # TODO - Raise exception if too much total size (4TB standard, 1TB HS) disk_set = [{'diskId': o.id, 'sizeGB': o.size} for o in self.disks] disk_set.append({'sizeGB': size, 'type': type, 'path': path}) self.disks.append( Disk(id=int(time.time()), parent=self, disk_obj={ 'sizeGB': size, 'partitionPaths': [path] }, session=self.session)) self.size = size self.server.dirty = True return (clc.v2.Requests(clc.v2.API.Call( 'PATCH', 'servers/%s/%s' % (self.server.alias, self.server.id), json.dumps([{ "op": "set", "member": "disks", "value": disk_set }]), session=self.session), alias=self.server.alias, session=self.session))
def __init__(self, id_, policy_obj=None, alias=None, session=None): self.id = id_ self.dirty = False self.session = session if alias: self.alias = alias else: self.alias = clc.v2.Account.GetAlias(session=self.session) if policy_obj: self.data = policy_obj else: try: self.Refresh() except clc.APIFailedResponse as e: if e.response_status_code == 404: raise (clc.CLCException( "Horizontal Autoscale Policy does not exist"))
def WaitUntilComplete(self,poll_freq=2): """Poll until status is completed. If status is 'notStarted' or 'executing' continue polling. If status is 'succeeded' return Else raise exception poll_freq option is in seconds """ while not self.time_completed: status = self.Status() if status == 'executing': if not self.time_executed: self.time_executed = time.time() elif status == 'succeeded': self.time_completed = time.time() elif status in ("failed", "resumed" or "unknown"): # TODO - need to ID best reaction for resumed status (e.g. manual intervention) self.time_completed = time.time() raise(clc.CLCException("%s %s execution %s" % (self.context_key,self.context_val,status))) time.sleep(poll_freq)
def __init__(self, requests_lst, alias=None): """Create Requests object. Treats one or more requests as an atomic unit. e.g. if performing a simulated group operation then succeed or fail as a group """ if alias: self.alias = alias else: self.alias = clc.v2.Account.GetAlias() self.requests = [] self.error_requests = [] self.success_requests = [] # Some requests_lst responses look different than others depending on the call (Create snapshot vs. delete snapshot) # Add min-wrapper onto those missing it if requests_lst is None: raise (Exception("Unexpected requests response")) elif 'isQueued' in requests_lst: requests_lst = [requests_lst] elif 'href' in requests_lst: requests_lst = [{'isQueued': True, 'links': [requests_lst]}] for r in requests_lst: if 'server' in r and len(r['server']) < 6: # Hopefully this captures only new server builds, TODO find a better way to ID these for link in r['links']: if re.search("/v2/servers/", link['href']): context_key = "newserver" context_val = link['href'] break elif 'server' in r: context_key = "server" context_val = r['server'] else: context_key = "Unknown" context_val = "Unknown" if r['isQueued']: self.requests.append( Request([ obj['id'] for obj in r['links'] if obj['rel'] == 'status' ][0], alias=self.alias, request_obj={ 'context_key': context_key, 'context_val': context_val })) else: # If we're dealing with a list of responses and we have an error with one I'm not sure how # folks would expect this to behave. If server is already in desired state thus the request # fails that shouldn't be an exception. If we're running against n tasks and just one has an # issue we need a reasonable way to report on the error but also follow the remaining tasks. # # For no-op failures we just won't create an object and our queue wait time will be faster. # For actual failures we'll wait until all tasks have reached a conclusion then ..... if r['errorMessage'] == "The server already in desired state.": pass elif r['errorMessage'] == "The operation cannot be queued because the server cannot be found or it is not in a valid state.": raise (Exception("do we pass on this or take action? %s" % r['errorMessage'])) else: # TODO - need to ID other reasons for not queuing and known reasons don't raise out of the # entire process raise (clc.CLCException( "%s '%s' not added to queue: %s" % (context_key, context_val, r['errorMessage'])))