From eb647cfea4cd1c1680f279ce311f899e17a0857e Mon Sep 17 00:00:00 2001 From: Stavros Sachtouris Date: Fri, 29 Nov 2013 13:03:46 +0200 Subject: [PATCH] Allow ports without device_id in lib + waits Refs: #4624, #4563 --- kamaki/cli/argument/__init__.py | 7 ++ kamaki/cli/commands/__init__.py | 2 +- kamaki/cli/commands/cyclades.py | 18 +++- kamaki/cli/commands/pithos.py | 48 +++++---- kamaki/clients/compute/__init__.py | 12 ++- kamaki/clients/cyclades/__init__.py | 192 ++++------------------------------- kamaki/clients/network/__init__.py | 51 +++++++++- 7 files changed, 123 insertions(+), 207 deletions(-) diff --git a/kamaki/cli/argument/__init__.py b/kamaki/cli/argument/__init__.py index 14000a4..bb491c6 100644 --- a/kamaki/cli/argument/__init__.py +++ b/kamaki/cli/argument/__init__.py @@ -52,6 +52,7 @@ class Argument(object): This is the top-level Argument class. It is suggested to extent this class into more specific argument types. """ + lvalue_delimiter = '/' def __init__(self, arity, help=None, parsed_name=None, default=None): self.arity = int(arity) @@ -86,6 +87,12 @@ class Argument(object): *self.parsed_name, dest=name, action=action, default=self.default, help=self.help) + @property + def lvalue(self): + """A printable form of the left value when calling an argument e.g., + --left-value=right-value""" + return (self.lvalue_delimiter or ' ').join(self.parsed_name or []) + class ConfigArgument(Argument): """Manage a kamaki configuration (file)""" diff --git a/kamaki/cli/commands/__init__.py b/kamaki/cli/commands/__init__.py index 7c91db4..3d6927b 100644 --- a/kamaki/cli/commands/__init__.py +++ b/kamaki/cli/commands/__init__.py @@ -263,7 +263,7 @@ class OutputFormatArgument(ValueArgument): else: raise CLIInvalidArgument( 'Invalid value %s for argument %s' % ( - newvalue, '/'.join(self.parsed_name)), + newvalue, self.lvalue), details=['Valid output formats: %s' % ', '.join(self.formats)]) diff --git a/kamaki/cli/commands/cyclades.py b/kamaki/cli/commands/cyclades.py index 315f07b..37ae2e2 100644 --- a/kamaki/cli/commands/cyclades.py +++ b/kamaki/cli/commands/cyclades.py @@ -415,13 +415,19 @@ class server_create(_init_cyclades, _optional_json, _server_wait): 'Connect server to network w. floating ip ( NETWORK_ID,IP )' '(can be repeated)', '--network-with-ip'), + automatic_ip=FlagArgument( + 'Automatically assign an IP to the server', '--automatic-ip') ) required = ('server_name', 'flavor_id', 'image_id') @errors.cyclades.cluster_size def _create_cluster(self, prefix, flavor_id, image_id, size): - networks = [dict(network=netid) for netid in ( - self['network_id'] or [])] + (self['network_id_and_ip'] or []) + if self['automatic_ip']: + networks = [] + else: + networks = [dict(network=netid) for netid in ( + (self['network_id'] or []) + (self['network_id_and_ip'] or []) + )] or None servers = [dict( name='%s%s' % (prefix, i if size > 1 else ''), flavor_id=flavor_id, @@ -473,6 +479,14 @@ class server_create(_init_cyclades, _optional_json, _server_wait): def main(self): super(self.__class__, self)._run() + if self['automatic_ip'] and ( + self['network_id'] or self['network_id_and_ip']): + raise CLIInvalidArgument('Invalid argument combination', details=[ + 'Argument %s should not be combined with other' % ( + self.arguments['automatic_ip'].lvalue), + 'network-related arguments i.e., %s or %s' % ( + self.arguments['network_id'].lvalue, + self.arguments['network_id_and_ip'].lvalue)]) self._run( name=self['server_name'], flavor_id=self['flavor_id'], diff --git a/kamaki/cli/commands/pithos.py b/kamaki/cli/commands/pithos.py index 1c76c2e..2481f65 100644 --- a/kamaki/cli/commands/pithos.py +++ b/kamaki/cli/commands/pithos.py @@ -377,14 +377,14 @@ class file_modify(_pithos_container): if self['publish'] and self['unpublish']: raise CLIInvalidArgument( 'Arguments %s and %s cannot be used together' % ( - '/'.join(self.arguments['publish'].parsed_name), - '/'.join(self.arguments['publish'].parsed_name))) + self.arguments['publish'].lvalue, + self.arguments['publish'].lvalue)) if self['no_permissions'] and ( self['uuid_for_read_permission'] or self[ 'uuid_for_write_permission']): raise CLIInvalidArgument( - '%s cannot be used with other permission arguments' % '/'.join( - self.arguments['no_permissions'].parsed_name)) + '%s cannot be used with other permission arguments' % ( + self.arguments['no_permissions'].lvalue)) self._run() @@ -555,8 +555,8 @@ class _source_destination(_pithos_container, _optional_output_cmd): self.dst_client.account, self.dst_client.container, dst_path), - 'Use %s to transfer overwrite' % ('/'.join( - self.arguments['force'].parsed_name))]) + 'Use %s to transfer overwrite' % ( + self.arguments['force'].lvalue)]) else: # One object transfer try: @@ -570,8 +570,7 @@ class _source_destination(_pithos_container, _optional_output_cmd): 'Missing specific path container %s' % self.container, importance=2, details=[ 'To transfer container contents %s' % ( - '/'.join(self.arguments[ - 'source_prefix'].parsed_name))]) + self.arguments['source_prefix'].lvalue)]) raise dst_path = self.dst_path or self.path dst_obj = dst_objects.get(dst_path or self.path, None) @@ -589,8 +588,7 @@ class _source_destination(_pithos_container, _optional_output_cmd): self.container, self.path), 'To recursively copy a directory, use', - ' %s' % ('/'.join( - self.arguments['source_prefix'].parsed_name)), + ' %s' % self.arguments['source_prefix'].lvalue, 'To create a file, use', ' /file create (general purpose)', ' /file mkdir (a directory object)']) @@ -607,8 +605,8 @@ class _source_destination(_pithos_container, _optional_output_cmd): self.dst_client.account, self.dst_client.container, dst_path), - 'Use %s to transfer overwrite' % ('/'.join( - self.arguments['force'].parsed_name))]) + 'Use %s to transfer overwrite' % ( + self.arguments['force'].lvalue)]) return pairs def _run(self, source_path_or_url, destination_path_or_url=''): @@ -873,8 +871,8 @@ class file_upload(_pithos_container, _optional_output_cmd): if path.isdir(lpath): if not self['recursive']: raise CLIError('%s is a directory' % lpath, details=[ - 'Use %s to upload directories & contents' % '/'.join( - self.arguments['recursive'].parsed_name)]) + 'Use %s to upload directories & contents' % ( + self.arguments['recursive'].lvalue)]) robj = self.client.container_get(path=rpath) if not self['overwrite']: if robj.json: @@ -1174,18 +1172,18 @@ class file_download(_pithos_container): elif path.exists(lpath): raise CLIError( 'Cannot overwrite %s' % lpath, - details=['To overwrite/resume, use %s' % '/'.join( - self.arguments['resume'].parsed_name)]) + details=['To overwrite/resume, use %s' % ( + self.arguments['resume'].lvalue)]) else: ret.append((opath, lpath, None)) elif self.path: raise CLIError( 'Remote object /%s/%s is a directory' % ( self.container, local_path), - details=['Use %s to download directories' % '/'.join( - self.arguments['recursive'].parsed_name)]) + details=['Use %s to download directories' % ( + self.arguments['recursive'].lvalue)]) else: - parsed_name = '/'.join(self.arguments['recursive'].parsed_name) + parsed_name = self.arguments['recursive'].lvalue raise CLIError( 'Cannot download container %s' % self.container, details=[ @@ -1197,8 +1195,8 @@ class file_download(_pithos_container): if path.exists(local_path) and not self['resume']: raise CLIError( 'Cannot overwrite local file %s' % (lpath), - details=['To overwrite/resume, use %s' % '/'.join( - self.arguments['resume'].parsed_name)]) + details=['To overwrite/resume, use %s' % ( + self.arguments['resume'].lvalue)]) ret.append((rpath, local_path, self['resume'])) for r, l, resume in ret: if r: @@ -1528,8 +1526,8 @@ class container_delete(_pithos_account): delimiter, msg = '/', 'Empty and d%s' % msg[1:] elif num_of_contents: raise CLIError('Container %s is not empty' % container, details=[ - 'Use %s to delete non-empty containers' % '/'.join( - self.arguments['recursive'].parsed_name)]) + 'Use %s to delete non-empty containers' % ( + self.arguments['recursive'].lvalue)]) if self['yes'] or self.ask_user(msg): if num_of_contents: self.client.del_container(delimiter=delimiter) @@ -1658,8 +1656,8 @@ class group_create(_pithos_group, _optional_json): else: raise CLISyntaxError( 'No valid users specified, use %s or %s' % ( - '/'.join(self.arguments['user_uuid'].parsed_name), - '/'.join(self.arguments['username'].parsed_name)), + self.arguments['user_uuid'].lvalue, + self.arguments['username'].lvalue), details=[ 'Check if a username or uuid is valid with', ' user uuid2username', 'OR', ' user username2uuid']) diff --git a/kamaki/clients/compute/__init__.py b/kamaki/clients/compute/__init__.py index 26c9381..9cff47a 100644 --- a/kamaki/clients/compute/__init__.py +++ b/kamaki/clients/compute/__init__.py @@ -128,6 +128,14 @@ class ComputeClient(ComputeRestClient): :param personality: a list of (file path, file contents) tuples, describing files to be injected into virtual server upon creation + :param networks: (list of dicts) Networks to connect to, list this: + "networks": [ + {"network": }, + {"network": , "fixed_ip": address}, + {"port": }, ...] + ATTENTION: Empty list is different to None. None means ' do not + mention it', empty list means 'automatically get an ip' + :returns: a dict with the new virtual server details :raises ClientError: wraps request errors @@ -141,8 +149,8 @@ class ComputeClient(ComputeRestClient): if personality: req['server']['personality'] = personality - if networks: - req['server']['networks'] = networks + if networks is not None: + req['server']['networks'] = networks or [] r = self.servers_post( json_data=req, diff --git a/kamaki/clients/cyclades/__init__.py b/kamaki/clients/cyclades/__init__.py index 207693d..c9b1632 100644 --- a/kamaki/clients/cyclades/__init__.py +++ b/kamaki/clients/cyclades/__init__.py @@ -61,6 +61,8 @@ class CycladesClient(CycladesRestClient, Waiter): {"network": }, {"network": , "fixed_ip": address}, {"port": }, ...] + ATTENTION: Empty list is different to None. None means ' do not + mention it', empty list means 'automatically get an ip' :returns: a dict with the new virtual server details @@ -76,7 +78,7 @@ class CycladesClient(CycladesRestClient, Waiter): return super(CycladesClient, self).create_server( name, flavor_id, image_id, - metadata=metadata, personality=personality) + metadata=metadata, personality=personality, networks=networks) def start_server(self, server_id): """Submit a startup request @@ -157,130 +159,6 @@ class CycladesClient(CycladesRestClient, Waiter): r = self.servers_stats_get(server_id) return r.json['stats'] - def list_networks(self, detail=False): - """ - :param detail: (bool) - - :returns: (list) id,name if not detail else full info per network - """ - detail = 'detail' if detail else '' - r = self.networks_get(command=detail) - return r.json['networks'] - - def list_network_nics(self, network_id): - """ - :param network_id: integer (str or int) - - :returns: (list) - """ - r = self.networks_get(network_id=network_id) - return r.json['network']['attachments'] - - def create_network( - self, name, - cidr=None, gateway=None, type=None, dhcp=False): - """ - :param name: (str) - - :param cidr: (str) - - :param geteway: (str) - - :param type: (str) if None, will use MAC_FILTERED as default - Valid values: CUSTOM, IP_LESS_ROUTED, MAC_FILTERED, PHYSICAL_VLAN - - :param dhcp: (bool) - - :returns: (dict) network detailed info - """ - net = dict(name=name) - if cidr: - net['cidr'] = cidr - if gateway: - net['gateway'] = gateway - net['type'] = type or 'MAC_FILTERED' - net['dhcp'] = True if dhcp else False - req = dict(network=net) - r = self.networks_post(json_data=req, success=202) - return r.json['network'] - - def get_network_details(self, network_id): - """ - :param network_id: integer (str or int) - - :returns: (dict) - """ - r = self.networks_get(network_id=network_id) - return r.json['network'] - - def update_network_name(self, network_id, new_name): - """ - :param network_id: integer (str or int) - - :param new_name: (str) - - :returns: (dict) response headers - """ - req = {'network': {'name': new_name}} - r = self.networks_put(network_id=network_id, json_data=req) - return r.headers - - def delete_network(self, network_id): - """ - :param network_id: integer (str or int) - - :returns: (dict) response headers - - :raises ClientError: 421 Network in use - """ - try: - r = self.networks_delete(network_id) - return r.headers - except ClientError as err: - if err.status == 421: - err.details = [ - 'Network may be still connected to at least one server'] - raise - - def connect_server(self, server_id, network_id): - """ Connect a server to a network - - :param server_id: integer (str or int) - - :param network_id: integer (str or int) - - :returns: (dict) response headers - """ - req = {'add': {'serverRef': server_id}} - r = self.networks_post(network_id, 'action', json_data=req) - return r.headers - - def disconnect_server(self, server_id, nic_id): - """ - :param server_id: integer (str or int) - - :param nic_id: (str) - - :returns: (int) the number of nics disconnected - """ - vm_nets = self.list_server_nics(server_id) - num_of_disconnections = 0 - for (nic_id, network_id) in [( - net['id'], - net['network_id']) for net in vm_nets if nic_id == net['id']]: - req = {'remove': {'attachment': '%s' % nic_id}} - self.networks_post(network_id, 'action', json_data=req) - num_of_disconnections += 1 - return num_of_disconnections - - def disconnect_network_nics(self, netid): - """ - :param netid: integer (str or int) - """ - for nic in self.list_network_nics(netid): - req = dict(remove=dict(attachment=nic)) - self.networks_post(netid, 'action', json_data=req) - def wait_server( self, server_id, current_status='BUILD', @@ -308,31 +186,6 @@ class CycladesClient(CycladesRestClient, Waiter): return self._wait( server_id, current_status, get_status, delay, max_wait, wait_cb) - def wait_network( - self, net_id, - current_status='PENDING', delay=1, max_wait=100, wait_cb=None): - """Wait for network while its status is current_status - - :param net_id: integer (str or int) - - :param current_status: (str) PENDING | ACTIVE | DELETED - - :param delay: time interval between retries - - :max_wait: (int) timeout in secconds - - :param wait_cb: if set a progressbar is used to show progress - - :returns: (str) the new mode if succesfull, (bool) False if timed out - """ - - def get_status(self, net_id): - r = self.get_network_details(net_id) - return r['status'], None - - return self._wait( - net_id, current_status, get_status, delay, max_wait, wait_cb) - def wait_firewall( self, server_id, current_status='DISABLED', delay=1, max_wait=100, wait_cb=None): @@ -358,7 +211,7 @@ class CycladesClient(CycladesRestClient, Waiter): server_id, current_status, get_status, delay, max_wait, wait_cb) -class CycladesNetworkClient(NetworkClient, Waiter): +class CycladesNetworkClient(NetworkClient): """Cyclades Network API extentions""" network_types = ( @@ -379,14 +232,16 @@ class CycladesNetworkClient(NetworkClient, Waiter): return r.json['network'] def create_port( - self, network_id, device_id, - security_groups=None, name=None, fixed_ips=None): - port = dict(network_id=network_id, device_id=device_id) + self, network_id, + device_id=None, security_groups=None, name=None, fixed_ips=None): + port = dict(network_id=network_id) + if device_id: + port['device_id'] = device_id if security_groups: port['security_groups'] = security_groups if name: port['name'] = name - for fixed_ip in fixed_ips: + for fixed_ip in fixed_ips or []: diff = set(['subnet_id', 'ip_address']).difference(fixed_ip) if diff: raise ValueError( @@ -396,24 +251,11 @@ class CycladesNetworkClient(NetworkClient, Waiter): r = self.ports_post(json_data=dict(port=port), success=201) return r.json['port'] - def wait_network( - self, net_id, - current_status='PENDING', delay=1, max_wait=100, wait_cb=None): + def create_floatingip(self, floating_network_id, floating_ip_address=''): + return super(CycladesNetworkClient, self).create_floatingip( + floating_network_id, floating_ip_address=floating_ip_address) - def get_status(self, net_id): - r = self.get_network_details(net_id) - return r['status'], None - - return self._wait( - net_id, current_status, get_status, delay, max_wait, wait_cb) - - def wait_port( - self, port_id, - current_status='PENDING', delay=1, max_wait=100, wait_cb=None): - - def get_status(self, net_id): - r = self.get_port_details(port_id) - return r['status'], None - - return self._wait( - port_id, current_status, get_status, delay, max_wait, wait_cb) + def update_floatingip(self, floating_network_id, floating_ip_address=''): + """To nullify something optional, use None""" + return super(CycladesNetworkClient, self).update_floatingip( + floating_network_id, floating_ip_address=floating_ip_address) diff --git a/kamaki/clients/network/__init__.py b/kamaki/clients/network/__init__.py index 68dc370..9c13382 100644 --- a/kamaki/clients/network/__init__.py +++ b/kamaki/clients/network/__init__.py @@ -31,11 +31,11 @@ # interpreted as representing official policies, either expressed # or implied, of GRNET S.A. -from kamaki.clients import ClientError +from kamaki.clients import ClientError, Waiter from kamaki.clients.network.rest_api import NetworkRestClient -class NetworkClient(NetworkRestClient): +class NetworkClient(NetworkRestClient, Waiter): """OpenStack Network API 2.0 client""" def list_networks(self): @@ -362,3 +362,50 @@ class NetworkClient(NetworkRestClient): def delete_floatingip(self, floatingip_id): r = self.floatingips_delete(floatingip_id, success=204) return r.headers + + # Wait methods + + def wait_network( + self, net_id, + current_status='PENDING', delay=1, max_wait=100, wait_cb=None): + + def get_status(self, net_id): + r = self.get_network_details(net_id) + return r['status'], None + + return self._wait( + net_id, current_status, get_status, delay, max_wait, wait_cb) + + def wait_subnet( + self, subnet_id, + current_status='PENDING', delay=1, max_wait=100, wait_cb=None): + + def get_status(self, subnet_id): + r = self.get_subnet_details(subnet_id) + return r['status'], None + + return self._wait( + subnet_id, current_status, get_status, delay, max_wait, wait_cb) + + def wait_port( + self, port_id, + current_status='PENDING', delay=1, max_wait=100, wait_cb=None): + + def get_status(self, net_id): + r = self.get_port_details(port_id) + return r['status'], None + + return self._wait( + port_id, current_status, get_status, delay, max_wait, wait_cb) + + def wait_floatingip( + self, floatingip_id, + current_status='PENDING', delay=1, max_wait=100, wait_cb=None): + + def get_status(self, floatingip_id): + r = self.get_network_details(floatingip_id) + return r['status'], None + + return self._wait( + floatingip_id, + current_status, get_status, delay, max_wait, wait_cb) -- 1.7.10.4