class SoftwareComponent(sc.SoftwareConfig): ''' A resource for describing and storing a software component. This resource is similar to OS::Heat::SoftwareConfig. In contrast to SoftwareConfig which allows for storing only one configuration (e.g. one script), SoftwareComponent allows for storing multiple configurations to address handling of all lifecycle hooks (CREATE, UPDATE, SUSPEND, RESUME, DELETE) for a software component in one place. This resource is backed by the persistence layer and the API of the SoftwareConfig resource, and only adds handling for the additional 'configs' property and attribute. ''' support_status = support.SupportStatus(version='2014.2') PROPERTIES = ( CONFIGS, INPUTS, OUTPUTS, OPTIONS, ) = ('configs', 'inputs', 'outputs', 'options') CONFIG_PROPERTIES = ( CONFIG_ACTIONS, CONFIG_CONFIG, CONFIG_TOOL, ) = ( 'actions', 'config', 'tool', ) ATTRIBUTES = (CONFIGS_ATTR, ) = ('configs', ) # properties schema for one entry in the 'configs' list config_schema = properties.Schema( properties.Schema.MAP, schema={ CONFIG_ACTIONS: properties.Schema( # Note: This properties schema allows for custom actions to be # specified, which will however require special handling in # in-instance hooks. By default, only the standard actions # stated below will be handled. properties.Schema.LIST, _('Lifecycle actions to which the configuration applies. ' 'The string values provided for this property can include ' 'the standard resource actions CREATE, DELETE, UPDATE, ' 'SUSPEND and RESUME supported by Heat.'), default=[resource.Resource.CREATE, resource.Resource.UPDATE], schema=properties.Schema(properties.Schema.STRING), constraints=[ constr.Length(min=1), ]), CONFIG_CONFIG: sc.SoftwareConfig.properties_schema[sc.SoftwareConfig.CONFIG], CONFIG_TOOL: properties.Schema( properties.Schema.STRING, _('The configuration tool used to actually apply the ' 'configuration on a server. This string property has ' 'to be understood by in-instance tools running inside ' 'deployed servers.'), required=True) }) properties_schema = { CONFIGS: properties.Schema( properties.Schema.LIST, _('The list of configurations for the different lifecycle actions ' 'of the represented software component.'), schema=config_schema, constraints=[constr.Length(min=1)], required=True), INPUTS: sc.SoftwareConfig.properties_schema[sc.SoftwareConfig.INPUTS], OUTPUTS: sc.SoftwareConfig.properties_schema[sc.SoftwareConfig.OUTPUTS], OPTIONS: sc.SoftwareConfig.properties_schema[sc.SoftwareConfig.OPTIONS], } def handle_create(self): props = dict(self.properties) props[self.NAME] = self.physical_resource_name() # use config property of SoftwareConfig to store configs list configs = self.properties[self.CONFIGS] props[self.CONFIG] = {self.CONFIGS: configs} # set 'group' to enable component processing by in-instance hook props[self.GROUP] = 'component' del props['configs'] sc = self.rpc_client().create_software_config(self.context, **props) self.resource_id_set(sc[rpc_api.SOFTWARE_CONFIG_ID]) def _resolve_attribute(self, name): ''' Retrieve attributes of the SoftwareComponent resource. 'configs' returns the list of configurations for the software component's lifecycle actions. If the attribute does not exist, an empty list is returned. ''' if name == self.CONFIGS_ATTR and self.resource_id: try: sc = self.rpc_client().show_software_config( self.context, self.resource_id) # configs list is stored in 'config' property of parent class # (see handle_create) return sc[rpc_api.SOFTWARE_CONFIG_CONFIG].get(self.CONFIGS) except Exception as ex: self.rpc_client().ignore_error_named(ex, 'NotFound') def validate(self): '''Validate SoftwareComponent properties consistency.''' super(SoftwareComponent, self).validate() # One lifecycle action (e.g. CREATE) can only be associated with one # config; otherwise a way to define ordering would be required. configs = self.properties.get(self.CONFIGS, []) config_actions = set() for config in configs: actions = config.get(self.CONFIG_ACTIONS) if any(action in config_actions for action in actions): msg = _('Defining more than one configuration for the same ' 'action in SoftwareComponent "%s" is not allowed.' ) % self.name raise exception.StackValidationFailed(message=msg) config_actions.update(actions)
class DockerContainer(resource.Resource): support_status = support.SupportStatus( status=support.UNSUPPORTED, message=_('This resource is not supported, use at your own risk.')) PROPERTIES = (DOCKER_ENDPOINT, HOSTNAME, USER, MEMORY, PORT_SPECS, PRIVILEGED, TTY, OPEN_STDIN, STDIN_ONCE, ENV, CMD, DNS, IMAGE, VOLUMES, VOLUMES_FROM, PORT_BINDINGS, LINKS, NAME, RESTART_POLICY, CAP_ADD, CAP_DROP, READ_ONLY, CPU_SHARES, DEVICES, CPU_SET) = ('docker_endpoint', 'hostname', 'user', 'memory', 'port_specs', 'privileged', 'tty', 'open_stdin', 'stdin_once', 'env', 'cmd', 'dns', 'image', 'volumes', 'volumes_from', 'port_bindings', 'links', 'name', 'restart_policy', 'cap_add', 'cap_drop', 'read_only', 'cpu_shares', 'devices', 'cpu_set') ATTRIBUTES = ( INFO, NETWORK_INFO, NETWORK_IP, NETWORK_GATEWAY, NETWORK_TCP_PORTS, NETWORK_UDP_PORTS, LOGS, LOGS_HEAD, LOGS_TAIL, ) = ( 'info', 'network_info', 'network_ip', 'network_gateway', 'network_tcp_ports', 'network_udp_ports', 'logs', 'logs_head', 'logs_tail', ) _RESTART_POLICY_KEYS = ( POLICY_NAME, POLICY_MAXIMUM_RETRY_COUNT, ) = ( 'Name', 'MaximumRetryCount', ) _DEVICES_KEYS = (PATH_ON_HOST, PATH_IN_CONTAINER, PERMISSIONS) = ('path_on_host', 'path_in_container', 'permissions') _CAPABILITIES = [ 'SETPCAP', 'SYS_MODULE', 'SYS_RAWIO', 'SYS_PACCT', 'SYS_ADMIN', 'SYS_NICE', 'SYS_RESOURCE', 'SYS_TIME', 'SYS_TTY_CONFIG', 'MKNOD', 'AUDIT_WRITE', 'AUDIT_CONTROL', 'MAC_OVERRIDE', 'MAC_ADMIN', 'NET_ADMIN', 'SYSLOG', 'CHOWN', 'NET_RAW', 'DAC_OVERRIDE', 'FOWNER', 'DAC_READ_SEARCH', 'FSETID', 'KILL', 'SETGID', 'SETUID', 'LINUX_IMMUTABLE', 'NET_BIND_SERVICE', 'NET_BROADCAST', 'IPC_LOCK', 'IPC_OWNER', 'SYS_CHROOT', 'SYS_PTRACE', 'SYS_BOOT', 'LEASE', 'SETFCAP', 'WAKE_ALARM', 'BLOCK_SUSPEND', 'ALL' ] properties_schema = { DOCKER_ENDPOINT: properties.Schema( properties.Schema.STRING, _('Docker daemon endpoint (by default the local docker daemon ' 'will be used).'), default=None), HOSTNAME: properties.Schema(properties.Schema.STRING, _('Hostname of the container.'), default=''), USER: properties.Schema(properties.Schema.STRING, _('Username or UID.'), default=''), MEMORY: properties.Schema(properties.Schema.INTEGER, _('Memory limit (Bytes).')), PORT_SPECS: properties.Schema(properties.Schema.LIST, _('TCP/UDP ports mapping.'), default=None), PORT_BINDINGS: properties.Schema( properties.Schema.MAP, _('TCP/UDP ports bindings.'), ), LINKS: properties.Schema( properties.Schema.MAP, _('Links to other containers.'), ), NAME: properties.Schema( properties.Schema.STRING, _('Name of the container.'), ), PRIVILEGED: properties.Schema(properties.Schema.BOOLEAN, _('Enable extended privileges.'), default=False), TTY: properties.Schema(properties.Schema.BOOLEAN, _('Allocate a pseudo-tty.'), default=False), OPEN_STDIN: properties.Schema(properties.Schema.BOOLEAN, _('Open stdin.'), default=False), STDIN_ONCE: properties.Schema( properties.Schema.BOOLEAN, _('If true, close stdin after the 1 attached client disconnects.'), default=False), ENV: properties.Schema( properties.Schema.LIST, _('Set environment variables.'), ), CMD: properties.Schema(properties.Schema.LIST, _('Command to run after spawning the container.'), default=[]), DNS: properties.Schema( properties.Schema.LIST, _('Set custom dns servers.'), ), IMAGE: properties.Schema(properties.Schema.STRING, _('Image name.')), VOLUMES: properties.Schema(properties.Schema.MAP, _('Create a bind mount.'), default={}), VOLUMES_FROM: properties.Schema(properties.Schema.LIST, _('Mount all specified volumes.'), default=''), RESTART_POLICY: properties.Schema( properties.Schema.MAP, _('Restart policies (only supported for API version >= 1.2.0).'), schema={ POLICY_NAME: properties.Schema( properties.Schema.STRING, _('The behavior to apply when the container exits.'), default='no', constraints=[ constraints.AllowedValues( ['no', 'on-failure', 'always']), ]), POLICY_MAXIMUM_RETRY_COUNT: properties.Schema(properties.Schema.INTEGER, _('A maximum restart count for the ' 'on-failure policy.'), default=0) }, default={}, support_status=support.SupportStatus(version='2015.1')), CAP_ADD: properties.Schema( properties.Schema.LIST, _('Be used to add kernel capabilities (only supported for ' 'API version >= 1.2.0).'), schema=properties.Schema( properties.Schema.STRING, _('The security features provided by Linux kernels.'), constraints=[ constraints.AllowedValues(_CAPABILITIES), ]), default=[], support_status=support.SupportStatus(version='2015.1')), CAP_DROP: properties.Schema( properties.Schema.LIST, _('Be used to drop kernel capabilities (only supported for ' 'API version >= 1.2.0).'), schema=properties.Schema( properties.Schema.STRING, _('The security features provided by Linux kernels.'), constraints=[ constraints.AllowedValues(_CAPABILITIES), ]), default=[], support_status=support.SupportStatus(version='2015.1')), READ_ONLY: properties.Schema( properties.Schema.BOOLEAN, _('If true, mount the container\'s root filesystem ' 'as read only (only supported for API version >= %s).') % MIN_API_VERSION_MAP['read_only'], default=False, support_status=support.SupportStatus(version='2015.1'), ), CPU_SHARES: properties.Schema( properties.Schema.INTEGER, _('Relative weight which determines the allocation of the CPU ' 'processing power(only supported for API version >= %s).') % MIN_API_VERSION_MAP['cpu_shares'], default=0, support_status=support.SupportStatus(version='5.0.0'), ), DEVICES: properties.Schema( properties.Schema.LIST, _('Device mappings (only supported for API version >= %s).') % MIN_API_VERSION_MAP['devices'], schema=properties.Schema( properties.Schema.MAP, schema={ PATH_ON_HOST: properties.Schema( properties.Schema.STRING, _('The device path on the host.'), constraints=[ constraints.Length(max=255), constraints.AllowedPattern(DEVICE_PATH_REGEX), ], required=True), PATH_IN_CONTAINER: properties.Schema( properties.Schema.STRING, _('The device path of the container' ' mappings to the host.'), constraints=[ constraints.Length(max=255), constraints.AllowedPattern(DEVICE_PATH_REGEX), ], ), PERMISSIONS: properties.Schema(properties.Schema.STRING, _('The permissions of the container to' ' read/write/create the devices.'), constraints=[ constraints.AllowedValues([ 'r', 'w', 'm', 'rw', 'rm', 'wm', 'rwm' ]), ], default='rwm') }), default=[], support_status=support.SupportStatus(version='5.0.0'), ), CPU_SET: properties.Schema( properties.Schema.STRING, _('The CPUs in which to allow execution ' '(only supported for API version >= %s).') % MIN_API_VERSION_MAP['cpu_set'], support_status=support.SupportStatus(version='5.0.0'), ) } attributes_schema = { INFO: attributes.Schema(_('Container info.')), NETWORK_INFO: attributes.Schema(_('Container network info.')), NETWORK_IP: attributes.Schema(_('Container ip address.')), NETWORK_GATEWAY: attributes.Schema(_('Container ip gateway.')), NETWORK_TCP_PORTS: attributes.Schema(_('Container TCP ports.')), NETWORK_UDP_PORTS: attributes.Schema(_('Container UDP ports.')), LOGS: attributes.Schema(_('Container logs.')), LOGS_HEAD: attributes.Schema(_('Container first logs line.')), LOGS_TAIL: attributes.Schema(_('Container last logs line.')), } def get_client(self): client = None if DOCKER_INSTALLED: endpoint = self.properties.get(self.DOCKER_ENDPOINT) if endpoint: client = docker.APIClient(endpoint) else: client = docker.APIClient() return client def _parse_networkinfo_ports(self, networkinfo): tcp = [] udp = [] for port, info in six.iteritems(networkinfo['Ports']): p = port.split('/') if not info or len(p) != 2 or 'HostPort' not in info[0]: continue port = info[0]['HostPort'] if p[1] == 'tcp': tcp.append(port) elif p[1] == 'udp': udp.append(port) return (','.join(tcp), ','.join(udp)) def _container_networkinfo(self, client, resource_id): info = client.inspect_container(self.resource_id) networkinfo = info['NetworkSettings'] ports = self._parse_networkinfo_ports(networkinfo) networkinfo['TcpPorts'] = ports[0] networkinfo['UdpPorts'] = ports[1] return networkinfo def _resolve_attribute(self, name): if not self.resource_id: return if name == 'info': client = self.get_client() return client.inspect_container(self.resource_id) if name == 'network_info': client = self.get_client() networkinfo = self._container_networkinfo(client, self.resource_id) return networkinfo if name == 'network_ip': client = self.get_client() networkinfo = self._container_networkinfo(client, self.resource_id) return networkinfo['IPAddress'] if name == 'network_gateway': client = self.get_client() networkinfo = self._container_networkinfo(client, self.resource_id) return networkinfo['Gateway'] if name == 'network_tcp_ports': client = self.get_client() networkinfo = self._container_networkinfo(client, self.resource_id) return networkinfo['TcpPorts'] if name == 'network_udp_ports': client = self.get_client() networkinfo = self._container_networkinfo(client, self.resource_id) return networkinfo['UdpPorts'] if name == 'logs': client = self.get_client() logs = client.logs(self.resource_id) return logs if name == 'logs_head': client = self.get_client() logs = client.logs(self.resource_id) return logs.split('\n')[0] if name == 'logs_tail': client = self.get_client() logs = client.logs(self.resource_id) return logs.split('\n').pop() def handle_create(self): create_args = { 'image': self.properties[self.IMAGE], 'command': self.properties[self.CMD], 'hostname': self.properties[self.HOSTNAME], 'user': self.properties[self.USER], 'stdin_open': self.properties[self.OPEN_STDIN], 'tty': self.properties[self.TTY], 'mem_limit': self.properties[self.MEMORY], 'ports': self.properties[self.PORT_SPECS], 'environment': self.properties[self.ENV], 'dns': self.properties[self.DNS], 'volumes': self.properties[self.VOLUMES], 'name': self.properties[self.NAME], 'cpu_shares': self.properties[self.CPU_SHARES], 'cpuset': self.properties[self.CPU_SET] } client = self.get_client() client.pull(self.properties[self.IMAGE]) result = client.create_container(**create_args) container_id = result['Id'] self.resource_id_set(container_id) start_args = {} if self.properties[self.PRIVILEGED]: start_args[self.PRIVILEGED] = True if self.properties[self.VOLUMES]: start_args['binds'] = self.properties[self.VOLUMES] if self.properties[self.VOLUMES_FROM]: start_args['volumes_from'] = self.properties[self.VOLUMES_FROM] if self.properties[self.PORT_BINDINGS]: start_args['port_bindings'] = self.properties[self.PORT_BINDINGS] if self.properties[self.LINKS]: start_args['links'] = self.properties[self.LINKS] if self.properties[self.RESTART_POLICY]: start_args['restart_policy'] = self.properties[self.RESTART_POLICY] if self.properties[self.CAP_ADD]: start_args['cap_add'] = self.properties[self.CAP_ADD] if self.properties[self.CAP_DROP]: start_args['cap_drop'] = self.properties[self.CAP_DROP] if self.properties[self.READ_ONLY]: start_args[self.READ_ONLY] = True if (self.properties[self.DEVICES] and not self.properties[self.PRIVILEGED]): start_args['devices'] = self._get_mapping_devices( self.properties[self.DEVICES]) client.start(container_id, **start_args) return container_id def _get_mapping_devices(self, devices): actual_devices = [] for device in devices: if device[self.PATH_IN_CONTAINER]: actual_devices.append(':'.join([ device[self.PATH_ON_HOST], device[self.PATH_IN_CONTAINER], device[self.PERMISSIONS] ])) else: actual_devices.append(':'.join([ device[self.PATH_ON_HOST], device[self.PATH_ON_HOST], device[self.PERMISSIONS] ])) return actual_devices def _get_container_status(self, container_id): client = self.get_client() info = client.inspect_container(container_id) return info['State'] def check_create_complete(self, container_id): status = self._get_container_status(container_id) exit_status = status.get('ExitCode') if exit_status is not None and exit_status != 0: logs = self.get_client().logs(self.resource_id) raise exception.ResourceInError(resource_status=self.FAILED, status_reason=logs) return status['Running'] def handle_delete(self): if self.resource_id is None: return client = self.get_client() try: client.kill(self.resource_id) except docker.errors.APIError as ex: if ex.response.status_code != 404: raise return self.resource_id def check_delete_complete(self, container_id): if container_id is None: return True try: status = self._get_container_status(container_id) if not status['Running']: client = self.get_client() client.remove_container(container_id) except docker.errors.APIError as ex: if ex.response.status_code == 404: return True raise return False def handle_suspend(self): if not self.resource_id: return client = self.get_client() client.stop(self.resource_id) return self.resource_id def check_suspend_complete(self, container_id): status = self._get_container_status(container_id) return (not status['Running']) def handle_resume(self): if not self.resource_id: return client = self.get_client() client.start(self.resource_id) return self.resource_id def check_resume_complete(self, container_id): status = self._get_container_status(container_id) return status['Running'] def validate(self): super(DockerContainer, self).validate() self._validate_arg_for_api_version() def _validate_arg_for_api_version(self): version = None for key in MIN_API_VERSION_MAP: if self.properties[key]: if not version: client = self.get_client() version = client.version()['ApiVersion'] min_version = MIN_API_VERSION_MAP[key] if compare_version(min_version, version) < 0: raise InvalidArgForVersion(arg=key, min_version=min_version)
class CloudLoadBalancer(resource.Resource): """Represents a Rackspace Cloud Loadbalancer.""" support_status = support.SupportStatus( status=support.UNSUPPORTED, message=_('This resource is not supported, use at your own risk.')) PROPERTIES = ( NAME, NODES, PROTOCOL, ACCESS_LIST, HALF_CLOSED, ALGORITHM, CONNECTION_LOGGING, METADATA, PORT, TIMEOUT, CONNECTION_THROTTLE, SESSION_PERSISTENCE, VIRTUAL_IPS, CONTENT_CACHING, HEALTH_MONITOR, SSL_TERMINATION, ERROR_PAGE, HTTPS_REDIRECT, ) = ( 'name', 'nodes', 'protocol', 'accessList', 'halfClosed', 'algorithm', 'connectionLogging', 'metadata', 'port', 'timeout', 'connectionThrottle', 'sessionPersistence', 'virtualIps', 'contentCaching', 'healthMonitor', 'sslTermination', 'errorPage', 'httpsRedirect', ) LB_UPDATE_PROPS = (NAME, ALGORITHM, PROTOCOL, HALF_CLOSED, PORT, TIMEOUT, HTTPS_REDIRECT) _NODE_KEYS = ( NODE_ADDRESSES, NODE_PORT, NODE_CONDITION, NODE_TYPE, NODE_WEIGHT, ) = ( 'addresses', 'port', 'condition', 'type', 'weight', ) _ACCESS_LIST_KEYS = ( ACCESS_LIST_ADDRESS, ACCESS_LIST_TYPE, ) = ( 'address', 'type', ) _CONNECTION_THROTTLE_KEYS = ( CONNECTION_THROTTLE_MAX_CONNECTION_RATE, CONNECTION_THROTTLE_MIN_CONNECTIONS, CONNECTION_THROTTLE_MAX_CONNECTIONS, CONNECTION_THROTTLE_RATE_INTERVAL, ) = ( 'maxConnectionRate', 'minConnections', 'maxConnections', 'rateInterval', ) _VIRTUAL_IP_KEYS = (VIRTUAL_IP_TYPE, VIRTUAL_IP_IP_VERSION, VIRTUAL_IP_ID) = ('type', 'ipVersion', 'id') _HEALTH_MONITOR_KEYS = ( HEALTH_MONITOR_ATTEMPTS_BEFORE_DEACTIVATION, HEALTH_MONITOR_DELAY, HEALTH_MONITOR_TIMEOUT, HEALTH_MONITOR_TYPE, HEALTH_MONITOR_BODY_REGEX, HEALTH_MONITOR_HOST_HEADER, HEALTH_MONITOR_PATH, HEALTH_MONITOR_STATUS_REGEX, ) = ( 'attemptsBeforeDeactivation', 'delay', 'timeout', 'type', 'bodyRegex', 'hostHeader', 'path', 'statusRegex', ) _HEALTH_MONITOR_CONNECT_KEYS = ( HEALTH_MONITOR_ATTEMPTS_BEFORE_DEACTIVATION, HEALTH_MONITOR_DELAY, HEALTH_MONITOR_TIMEOUT, HEALTH_MONITOR_TYPE, ) _SSL_TERMINATION_KEYS = ( SSL_TERMINATION_SECURE_PORT, SSL_TERMINATION_PRIVATEKEY, SSL_TERMINATION_CERTIFICATE, SSL_TERMINATION_INTERMEDIATE_CERTIFICATE, SSL_TERMINATION_SECURE_TRAFFIC_ONLY, ) = ( 'securePort', 'privatekey', 'certificate', 'intermediateCertificate', 'secureTrafficOnly', ) ATTRIBUTES = (PUBLIC_IP, VIPS) = ('PublicIp', 'virtualIps') ALGORITHMS = [ "LEAST_CONNECTIONS", "RANDOM", "ROUND_ROBIN", "WEIGHTED_LEAST_CONNECTIONS", "WEIGHTED_ROUND_ROBIN" ] _health_monitor_schema = { HEALTH_MONITOR_ATTEMPTS_BEFORE_DEACTIVATION: properties.Schema(properties.Schema.NUMBER, required=True, constraints=[ constraints.Range(1, 10), ]), HEALTH_MONITOR_DELAY: properties.Schema(properties.Schema.NUMBER, required=True, constraints=[ constraints.Range(1, 3600), ]), HEALTH_MONITOR_TIMEOUT: properties.Schema(properties.Schema.NUMBER, required=True, constraints=[ constraints.Range(1, 300), ]), HEALTH_MONITOR_TYPE: properties.Schema(properties.Schema.STRING, required=True, constraints=[ constraints.AllowedValues( ['CONNECT', 'HTTP', 'HTTPS']), ]), HEALTH_MONITOR_BODY_REGEX: properties.Schema(properties.Schema.STRING), HEALTH_MONITOR_HOST_HEADER: properties.Schema(properties.Schema.STRING), HEALTH_MONITOR_PATH: properties.Schema(properties.Schema.STRING), HEALTH_MONITOR_STATUS_REGEX: properties.Schema(properties.Schema.STRING), } properties_schema = { NAME: properties.Schema(properties.Schema.STRING, update_allowed=True), NODES: properties.Schema( properties.Schema.LIST, schema=properties.Schema( properties.Schema.MAP, schema={ NODE_ADDRESSES: properties.Schema( properties.Schema.LIST, required=True, description=(_("IP addresses for the load balancer " "node. Must have at least one " "address.")), schema=properties.Schema(properties.Schema.STRING)), NODE_PORT: properties.Schema(properties.Schema.INTEGER, required=True), NODE_CONDITION: properties.Schema(properties.Schema.STRING, default='ENABLED', constraints=[ constraints.AllowedValues([ 'ENABLED', 'DISABLED', 'DRAINING' ]), ]), NODE_TYPE: properties.Schema(properties.Schema.STRING, constraints=[ constraints.AllowedValues( ['PRIMARY', 'SECONDARY']), ]), NODE_WEIGHT: properties.Schema(properties.Schema.NUMBER, constraints=[ constraints.Range(1, 100), ]), }, ), required=True, update_allowed=True), PROTOCOL: properties.Schema(properties.Schema.STRING, required=True, constraints=[ constraints.AllowedValues([ 'DNS_TCP', 'DNS_UDP', 'FTP', 'HTTP', 'HTTPS', 'IMAPS', 'IMAPv4', 'LDAP', 'LDAPS', 'MYSQL', 'POP3', 'POP3S', 'SMTP', 'TCP', 'TCP_CLIENT_FIRST', 'UDP', 'UDP_STREAM', 'SFTP' ]), ], update_allowed=True), ACCESS_LIST: properties.Schema(properties.Schema.LIST, schema=properties.Schema( properties.Schema.MAP, schema={ ACCESS_LIST_ADDRESS: properties.Schema(properties.Schema.STRING, required=True), ACCESS_LIST_TYPE: properties.Schema( properties.Schema.STRING, required=True, constraints=[ constraints.AllowedValues( ['ALLOW', 'DENY']), ]), }, )), HALF_CLOSED: properties.Schema(properties.Schema.BOOLEAN, update_allowed=True), ALGORITHM: properties.Schema(properties.Schema.STRING, constraints=[constraints.AllowedValues(ALGORITHMS)], update_allowed=True), CONNECTION_LOGGING: properties.Schema(properties.Schema.BOOLEAN, update_allowed=True), METADATA: properties.Schema(properties.Schema.MAP, update_allowed=True), PORT: properties.Schema(properties.Schema.INTEGER, required=True, update_allowed=True), TIMEOUT: properties.Schema(properties.Schema.NUMBER, constraints=[ constraints.Range(1, 120), ], update_allowed=True), CONNECTION_THROTTLE: properties.Schema(properties.Schema.MAP, schema={ CONNECTION_THROTTLE_MAX_CONNECTION_RATE: properties.Schema(properties.Schema.NUMBER, constraints=[ constraints.Range( 0, 100000), ]), CONNECTION_THROTTLE_MIN_CONNECTIONS: properties.Schema(properties.Schema.INTEGER, constraints=[ constraints.Range(1, 1000), ]), CONNECTION_THROTTLE_MAX_CONNECTIONS: properties.Schema(properties.Schema.INTEGER, constraints=[ constraints.Range( 1, 100000), ]), CONNECTION_THROTTLE_RATE_INTERVAL: properties.Schema(properties.Schema.NUMBER, constraints=[ constraints.Range(1, 3600), ]), }, update_allowed=True), SESSION_PERSISTENCE: properties.Schema(properties.Schema.STRING, constraints=[ constraints.AllowedValues( ['HTTP_COOKIE', 'SOURCE_IP']), ], update_allowed=True), VIRTUAL_IPS: properties.Schema( properties.Schema.LIST, schema=properties.Schema( properties.Schema.MAP, schema={ VIRTUAL_IP_TYPE: properties.Schema( properties.Schema.STRING, "The type of VIP (public or internal). This property" " cannot be specified if 'id' is specified. This " "property must be specified if id is not specified.", constraints=[ constraints.AllowedValues(['SERVICENET', 'PUBLIC']), ]), VIRTUAL_IP_IP_VERSION: properties.Schema( properties.Schema.STRING, "IP version of the VIP. This property cannot be " "specified if 'id' is specified. This property must " "be specified if id is not specified.", constraints=[ constraints.AllowedValues(['IPV6', 'IPV4']), ]), VIRTUAL_IP_ID: properties.Schema( properties.Schema.NUMBER, "ID of a shared VIP to use instead of creating a " "new one. This property cannot be specified if type" " or version is specified.") }, ), required=True, constraints=[constraints.Length(min=1)]), CONTENT_CACHING: properties.Schema(properties.Schema.STRING, constraints=[ constraints.AllowedValues( ['ENABLED', 'DISABLED']), ], update_allowed=True), HEALTH_MONITOR: properties.Schema(properties.Schema.MAP, schema=_health_monitor_schema, update_allowed=True), SSL_TERMINATION: properties.Schema( properties.Schema.MAP, schema={ SSL_TERMINATION_SECURE_PORT: properties.Schema(properties.Schema.INTEGER, default=443), SSL_TERMINATION_PRIVATEKEY: properties.Schema(properties.Schema.STRING, required=True), SSL_TERMINATION_CERTIFICATE: properties.Schema(properties.Schema.STRING, required=True), # only required if configuring intermediate ssl termination # add to custom validation SSL_TERMINATION_INTERMEDIATE_CERTIFICATE: properties.Schema(properties.Schema.STRING), # pyrax will default to false SSL_TERMINATION_SECURE_TRAFFIC_ONLY: properties.Schema(properties.Schema.BOOLEAN, default=False), }, update_allowed=True), ERROR_PAGE: properties.Schema(properties.Schema.STRING, update_allowed=True), HTTPS_REDIRECT: properties.Schema( properties.Schema.BOOLEAN, _("Enables or disables HTTP to HTTPS redirection for the load " "balancer. When enabled, any HTTP request returns status code " "301 (Moved Permanently), and the requester is redirected to " "the requested URL via the HTTPS protocol on port 443. Only " "available for HTTPS protocol (port=443), or HTTP protocol with " "a properly configured SSL termination (secureTrafficOnly=true, " "securePort=443)."), update_allowed=True, default=False, support_status=support.SupportStatus(version="2015.1")) } attributes_schema = { PUBLIC_IP: attributes.Schema(_('Public IP address of the specified instance.')), VIPS: attributes.Schema(_("A list of assigned virtual ip addresses")) } ACTIVE_STATUS = 'ACTIVE' DELETED_STATUS = 'DELETED' PENDING_DELETE_STATUS = 'PENDING_DELETE' def __init__(self, name, json_snippet, stack): super(CloudLoadBalancer, self).__init__(name, json_snippet, stack) self.clb = self.cloud_lb() def cloud_lb(self): return self.client('cloud_lb') def _setup_properties(self, properties, function): """Use defined schema properties as kwargs for loadbalancer objects.""" if properties and function: return [ function(**self._remove_none(item_dict)) for item_dict in properties ] elif function: return [function()] def _alter_properties_for_api(self): """Set up required, but useless, key/value pairs. The following properties have useless key/value pairs which must be passed into the api. Set them up to make template definition easier. """ session_persistence = None if self.SESSION_PERSISTENCE in self.properties.data: session_persistence = { 'persistenceType': self.properties[self.SESSION_PERSISTENCE] } connection_logging = None if self.CONNECTION_LOGGING in self.properties.data: connection_logging = { "enabled": self.properties[self.CONNECTION_LOGGING] } metadata = None if self.METADATA in self.properties.data: metadata = [{ 'key': k, 'value': v } for k, v in six.iteritems(self.properties[self.METADATA])] return (session_persistence, connection_logging, metadata) def _check_active(self): """Update the loadbalancer state, check the status.""" loadbalancer = self.clb.get(self.resource_id) if loadbalancer.status == self.ACTIVE_STATUS: return True else: return False def _valid_HTTPS_redirect_with_HTTP_prot(self): """Determine if HTTPS redirect is valid when protocol is HTTP""" proto = self.properties[self.PROTOCOL] redir = self.properties[self.HTTPS_REDIRECT] termcfg = self.properties.get(self.SSL_TERMINATION) or {} seconly = termcfg.get(self.SSL_TERMINATION_SECURE_TRAFFIC_ONLY, False) secport = termcfg.get(self.SSL_TERMINATION_SECURE_PORT, 0) if (redir and (proto == "HTTP") and seconly and (secport == 443)): return True return False def _configure_post_creation(self, loadbalancer): """Configure all load balancer properties post creation. These properties can only be set after the load balancer is created. """ if self.properties[self.ACCESS_LIST]: while not self._check_active(): yield loadbalancer.add_access_list(self.properties[self.ACCESS_LIST]) if self.properties[self.ERROR_PAGE]: while not self._check_active(): yield loadbalancer.set_error_page(self.properties[self.ERROR_PAGE]) if self.properties[self.SSL_TERMINATION]: while not self._check_active(): yield ssl_term = self.properties[self.SSL_TERMINATION] loadbalancer.add_ssl_termination( ssl_term[self.SSL_TERMINATION_SECURE_PORT], ssl_term[self.SSL_TERMINATION_PRIVATEKEY], ssl_term[self.SSL_TERMINATION_CERTIFICATE], intermediateCertificate=ssl_term[ self.SSL_TERMINATION_INTERMEDIATE_CERTIFICATE], enabled=True, secureTrafficOnly=ssl_term[ self.SSL_TERMINATION_SECURE_TRAFFIC_ONLY]) if self._valid_HTTPS_redirect_with_HTTP_prot(): while not self._check_active(): yield loadbalancer.update(httpsRedirect=True) if self.CONTENT_CACHING in self.properties: enabled = self.properties[self.CONTENT_CACHING] == 'ENABLED' while not self._check_active(): yield loadbalancer.content_caching = enabled def _process_node(self, node): if not node.get(self.NODE_ADDRESSES): yield node else: for addr in node.get(self.NODE_ADDRESSES): norm_node = copy.deepcopy(node) norm_node['address'] = addr del norm_node[self.NODE_ADDRESSES] yield norm_node def _process_nodes(self, node_list): node_itr = six.moves.map(self._process_node, node_list) return itertools.chain.from_iterable(node_itr) def _validate_https_redirect(self): redir = self.properties[self.HTTPS_REDIRECT] proto = self.properties[self.PROTOCOL] if (redir and (proto != "HTTPS") and not self._valid_HTTPS_redirect_with_HTTP_prot()): message = _("HTTPS redirect is only available for the HTTPS " "protocol (port=443), or the HTTP protocol with " "a properly configured SSL termination " "(secureTrafficOnly=true, securePort=443).") raise exception.StackValidationFailed(message=message) def handle_create(self): node_list = self._process_nodes(self.properties.get(self.NODES)) nodes = [self.clb.Node(**node) for node in node_list] vips = self.properties.get(self.VIRTUAL_IPS) virtual_ips = self._setup_properties(vips, self.clb.VirtualIP) (session_persistence, connection_logging, metadata) = self._alter_properties_for_api() lb_body = { 'port': self.properties[self.PORT], 'protocol': self.properties[self.PROTOCOL], 'nodes': nodes, 'virtual_ips': virtual_ips, 'algorithm': self.properties.get(self.ALGORITHM), 'halfClosed': self.properties.get(self.HALF_CLOSED), 'connectionThrottle': self.properties.get(self.CONNECTION_THROTTLE), 'metadata': metadata, 'healthMonitor': self.properties.get(self.HEALTH_MONITOR), 'sessionPersistence': session_persistence, 'timeout': self.properties.get(self.TIMEOUT), 'connectionLogging': connection_logging, self.HTTPS_REDIRECT: self.properties[self.HTTPS_REDIRECT] } if self._valid_HTTPS_redirect_with_HTTP_prot(): lb_body[self.HTTPS_REDIRECT] = False self._validate_https_redirect() lb_name = (self.properties.get(self.NAME) or self.physical_resource_name()) LOG.debug("Creating loadbalancer: %s" % {lb_name: lb_body}) loadbalancer = self.clb.create(lb_name, **lb_body) self.resource_id_set(str(loadbalancer.id)) post_create = scheduler.TaskRunner(self._configure_post_creation, loadbalancer) post_create(timeout=600) return loadbalancer def check_create_complete(self, loadbalancer): return self._check_active() def handle_check(self): loadbalancer = self.clb.get(self.resource_id) if not self._check_active(): raise exception.Error( _("Cloud LoadBalancer is not ACTIVE " "(was: %s)") % loadbalancer.status) def handle_update(self, json_snippet, tmpl_diff, prop_diff): """Add and remove nodes specified in the prop_diff.""" lb = self.clb.get(self.resource_id) checkers = [] if self.NODES in prop_diff: updated_nodes = prop_diff[self.NODES] checkers.extend(self._update_nodes(lb, updated_nodes)) updated_props = {} for prop in six.iterkeys(prop_diff): if prop in self.LB_UPDATE_PROPS: updated_props[prop] = prop_diff[prop] if updated_props: checkers.append(self._update_lb_properties(lb, updated_props)) if self.HEALTH_MONITOR in prop_diff: updated_hm = prop_diff[self.HEALTH_MONITOR] checkers.append(self._update_health_monitor(lb, updated_hm)) if self.SESSION_PERSISTENCE in prop_diff: updated_sp = prop_diff[self.SESSION_PERSISTENCE] checkers.append(self._update_session_persistence(lb, updated_sp)) if self.SSL_TERMINATION in prop_diff: updated_ssl_term = prop_diff[self.SSL_TERMINATION] checkers.append(self._update_ssl_termination(lb, updated_ssl_term)) if self.METADATA in prop_diff: updated_metadata = prop_diff[self.METADATA] checkers.append(self._update_metadata(lb, updated_metadata)) if self.ERROR_PAGE in prop_diff: updated_errorpage = prop_diff[self.ERROR_PAGE] checkers.append(self._update_errorpage(lb, updated_errorpage)) if self.CONNECTION_LOGGING in prop_diff: updated_cl = prop_diff[self.CONNECTION_LOGGING] checkers.append(self._update_connection_logging(lb, updated_cl)) if self.CONNECTION_THROTTLE in prop_diff: updated_ct = prop_diff[self.CONNECTION_THROTTLE] checkers.append(self._update_connection_throttle(lb, updated_ct)) if self.CONTENT_CACHING in prop_diff: updated_cc = prop_diff[self.CONTENT_CACHING] checkers.append(self._update_content_caching(lb, updated_cc)) return checkers def _update_nodes(self, lb, updated_nodes): @retry_if_immutable def add_nodes(lb, new_nodes): lb.add_nodes(new_nodes) @retry_if_immutable def remove_node(known, node): known[node].delete() @retry_if_immutable def update_node(known, node): known[node].update() checkers = [] current_nodes = lb.nodes diff_nodes = self._process_nodes(updated_nodes) # Loadbalancers can be uniquely identified by address and # port. Old is a dict of all nodes the loadbalancer # currently knows about. old = dict(("{0.address}{0.port}".format(node), node) for node in current_nodes) # New is a dict of the nodes the loadbalancer will know # about after this update. new = dict(("%s%s" % (node["address"], node[self.NODE_PORT]), node) for node in diff_nodes) old_set = set(six.iterkeys(old)) new_set = set(six.iterkeys(new)) deleted = old_set.difference(new_set) added = new_set.difference(old_set) updated = new_set.intersection(old_set) if len(current_nodes) + len(added) - len(deleted) < 1: raise ValueError( _("The loadbalancer:%s requires at least one " "node.") % self.name) """ Add loadbalancers in the new map that are not in the old map. Add before delete to avoid deleting the last node and getting in an invalid state. """ new_nodes = [self.clb.Node(**new[lb_node]) for lb_node in added] if new_nodes: checkers.append(scheduler.TaskRunner(add_nodes, lb, new_nodes)) # Delete loadbalancers in the old dict that are not in the # new dict. for node in deleted: checkers.append(scheduler.TaskRunner(remove_node, old, node)) # Update nodes that have been changed for node in updated: node_changed = False for attribute in six.iterkeys(new[node]): new_value = new[node][attribute] if new_value and new_value != getattr(old[node], attribute): node_changed = True setattr(old[node], attribute, new_value) if node_changed: checkers.append(scheduler.TaskRunner(update_node, old, node)) return checkers def _update_lb_properties(self, lb, updated_props): @retry_if_immutable def update_lb(): lb.update(**updated_props) return scheduler.TaskRunner(update_lb) def _update_health_monitor(self, lb, updated_hm): @retry_if_immutable def add_health_monitor(): lb.add_health_monitor(**updated_hm) @retry_if_immutable def delete_health_monitor(): lb.delete_health_monitor() if updated_hm is None: return scheduler.TaskRunner(delete_health_monitor) else: # Adding a health monitor is a destructive, so there's # no need to delete, then add return scheduler.TaskRunner(add_health_monitor) def _update_session_persistence(self, lb, updated_sp): @retry_if_immutable def add_session_persistence(): lb.session_persistence = updated_sp @retry_if_immutable def delete_session_persistence(): lb.session_persistence = '' if updated_sp is None: return scheduler.TaskRunner(delete_session_persistence) else: # Adding session persistence is destructive return scheduler.TaskRunner(add_session_persistence) def _update_ssl_termination(self, lb, updated_ssl_term): @retry_if_immutable def add_ssl_termination(): lb.add_ssl_termination(**updated_ssl_term) @retry_if_immutable def delete_ssl_termination(): lb.delete_ssl_termination() if updated_ssl_term is None: return scheduler.TaskRunner(delete_ssl_termination) else: # Adding SSL termination is destructive return scheduler.TaskRunner(add_ssl_termination) def _update_metadata(self, lb, updated_metadata): @retry_if_immutable def add_metadata(): lb.set_metadata(updated_metadata) @retry_if_immutable def delete_metadata(): lb.delete_metadata() if updated_metadata is None: return scheduler.TaskRunner(delete_metadata) else: return scheduler.TaskRunner(add_metadata) def _update_errorpage(self, lb, updated_errorpage): @retry_if_immutable def add_errorpage(): lb.set_error_page(updated_errorpage) @retry_if_immutable def delete_errorpage(): lb.clear_error_page() if updated_errorpage is None: return scheduler.TaskRunner(delete_errorpage) else: return scheduler.TaskRunner(add_errorpage) def _update_connection_logging(self, lb, updated_cl): @retry_if_immutable def enable_connection_logging(): lb.connection_logging = True @retry_if_immutable def disable_connection_logging(): lb.connection_logging = False if updated_cl: return scheduler.TaskRunner(enable_connection_logging) else: return scheduler.TaskRunner(disable_connection_logging) def _update_connection_throttle(self, lb, updated_ct): @retry_if_immutable def add_connection_throttle(): lb.add_connection_throttle(**updated_ct) @retry_if_immutable def delete_connection_throttle(): lb.delete_connection_throttle() if updated_ct is None: return scheduler.TaskRunner(delete_connection_throttle) else: return scheduler.TaskRunner(add_connection_throttle) def _update_content_caching(self, lb, updated_cc): @retry_if_immutable def enable_content_caching(): lb.content_caching = True @retry_if_immutable def disable_content_caching(): lb.content_caching = False if updated_cc == 'ENABLED': return scheduler.TaskRunner(enable_content_caching) else: return scheduler.TaskRunner(disable_content_caching) def check_update_complete(self, checkers): """Push all checkers to completion in list order.""" for checker in checkers: if not checker.started(): checker.start() if not checker.step(): return False return True def check_delete_complete(self, *args): if self.resource_id is None: return True try: loadbalancer = self.clb.get(self.resource_id) except NotFound: return True if loadbalancer.status == self.DELETED_STATUS: return True elif loadbalancer.status == self.PENDING_DELETE_STATUS: return False else: try: loadbalancer.delete() except Exception as exc: if lb_immutable(exc): return False raise return False def _remove_none(self, property_dict): """Remove None values that would cause schema validation problems. These are values that may be initialized to None. """ return dict((key, value) for (key, value) in six.iteritems(property_dict) if value is not None) def validate(self): """Validate any of the provided params.""" res = super(CloudLoadBalancer, self).validate() if res: return res if self.properties.get(self.HALF_CLOSED): if not (self.properties[self.PROTOCOL] == 'TCP' or self.properties[self.PROTOCOL] == 'TCP_CLIENT_FIRST'): message = (_('The %s property is only available for the TCP ' 'or TCP_CLIENT_FIRST protocols') % self.HALF_CLOSED) raise exception.StackValidationFailed(message=message) # health_monitor connect and http types require completely different # schema if self.properties.get(self.HEALTH_MONITOR): prop_val = self.properties[self.HEALTH_MONITOR] health_monitor = self._remove_none(prop_val) schema = self._health_monitor_schema if health_monitor[self.HEALTH_MONITOR_TYPE] == 'CONNECT': schema = dict((k, v) for k, v in schema.items() if k in self._HEALTH_MONITOR_CONNECT_KEYS) properties.Properties(schema, health_monitor, function.resolve, self.name).validate() # validate if HTTPS_REDIRECT is true self._validate_https_redirect() # if a vip specifies and id, it can't specify version or type; # otherwise version and type are required for vip in self.properties.get(self.VIRTUAL_IPS, []): has_id = vip.get(self.VIRTUAL_IP_ID) is not None has_version = vip.get(self.VIRTUAL_IP_IP_VERSION) is not None has_type = vip.get(self.VIRTUAL_IP_TYPE) is not None if has_id: if (has_version or has_type): message = _("Cannot specify type or version if VIP id is" " specified.") raise exception.StackValidationFailed(message=message) elif not (has_version and has_type): message = _("Must specify VIP type and version if no id " "specified.") raise exception.StackValidationFailed(message=message) def _public_ip(self, lb): for ip in lb.virtual_ips: if ip.type == 'PUBLIC': return six.text_type(ip.address) def _resolve_attribute(self, key): if self.resource_id: lb = self.clb.get(self.resource_id) attribute_function = { self.PUBLIC_IP: self._public_ip(lb), self.VIPS: [{ "id": vip.id, "type": vip.type, "ip_version": vip.ip_version, "address": vip.address } for vip in lb.virtual_ips] } if key not in attribute_function: raise exception.InvalidTemplateAttribute(resource=self.name, key=key) function = attribute_function[key] LOG.info(_LI('%(name)s.GetAtt(%(key)s) == %(function)s'), { 'name': self.name, 'key': key, 'function': function }) return function
def test_length_max_schema(self): d = {'length': {'max': 10}, 'description': 'a length range'} r = constraints.Length(max=10, description='a length range') self.assertEqual(d, dict(r))
class CloudNetwork(resource.Resource): """ A resource for creating Rackspace Cloud Networks. See http://www.rackspace.com/cloud/networks/ for service documentation. """ PROPERTIES = (LABEL, CIDR) = ("label", "cidr") properties_schema = { LABEL: properties.Schema(properties.Schema.STRING, _("The name of the network."), required=True, constraints=[constraints.Length(min=3, max=64)]), CIDR: properties.Schema( properties.Schema.STRING, _("The IP block from which to allocate the network. For example, " "172.16.0.0/24 or 2001:DB8::/64."), required=True) } attributes_schema = { "cidr": _("The CIDR for an isolated private network."), "label": _("The name of the network.") } def __init__(self, name, json_snippet, stack): resource.Resource.__init__(self, name, json_snippet, stack) self._network = None def network(self): if self.resource_id and not self._network: try: self._network = self.cloud_networks().get(self.resource_id) except NotFound: logger.warn( _("Could not find network %s but resource id " "is set.") % self.resource_id) return self._network def cloud_networks(self): return self.stack.clients.cloud_networks() def handle_create(self): cnw = self.cloud_networks().create(label=self.properties[self.LABEL], cidr=self.properties[self.CIDR]) self.resource_id_set(cnw.id) def handle_delete(self): net = self.network() if net: net.delete() return net def check_delete_complete(self, network): if network: try: network.get() except NotFound: return True else: return False return True def validate(self): super(CloudNetwork, self).validate() try: netaddr.IPNetwork(self.properties[self.CIDR]) except netaddr.core.AddrFormatError: raise exception.StackValidationFailed(message=_("Invalid cidr")) def _resolve_attribute(self, name): net = self.network() if net: return unicode(getattr(net, name)) return ""
def test_length_max_fail(self): l = constraints.Length(max=5, description='a range') self.assertRaises(ValueError, l.validate, 'abcdef')
def test_schema_validate_good(self): s = constraints.Schema(constraints.Schema.STRING, 'A string', default='wibble', constraints=[constraints.Length(4, 8)]) self.assertIsNone(s.validate())
class OSDBInstance(resource.Resource): """OpenStack cloud database instance resource. Trove is Database as a Service for OpenStack. It's designed to run entirely on OpenStack, with the goal of allowing users to quickly and easily utilize the features of a relational or non-relational database without the burden of handling complex administrative tasks. """ support_status = support.SupportStatus(version='2014.1') TROVE_STATUS = ( ERROR, FAILED, ACTIVE, ) = ( 'ERROR', 'FAILED', 'ACTIVE', ) TROVE_STATUS_REASON = { FAILED: _('The database instance was created, but heat failed to set ' 'up the datastore. If a database instance is in the FAILED ' 'state, it should be deleted and a new one should be ' 'created.'), ERROR: _('The last operation for the database instance failed due to ' 'an error.'), } BAD_STATUSES = (ERROR, FAILED) PROPERTIES = ( NAME, FLAVOR, SIZE, DATABASES, USERS, AVAILABILITY_ZONE, RESTORE_POINT, DATASTORE_TYPE, DATASTORE_VERSION, NICS, REPLICA_OF, REPLICA_COUNT, ) = ('name', 'flavor', 'size', 'databases', 'users', 'availability_zone', 'restore_point', 'datastore_type', 'datastore_version', 'networks', 'replica_of', 'replica_count') _DATABASE_KEYS = ( DATABASE_CHARACTER_SET, DATABASE_COLLATE, DATABASE_NAME, ) = ( 'character_set', 'collate', 'name', ) _USER_KEYS = ( USER_NAME, USER_PASSWORD, USER_HOST, USER_DATABASES, ) = ( 'name', 'password', 'host', 'databases', ) _NICS_KEYS = (NET, PORT, V4_FIXED_IP) = ('network', 'port', 'fixed_ip') ATTRIBUTES = ( HOSTNAME, HREF, ) = ( 'hostname', 'href', ) properties_schema = { NAME: properties.Schema(properties.Schema.STRING, _('Name of the DB instance to create.'), update_allowed=True, constraints=[ constraints.Length(max=255), ]), FLAVOR: properties.Schema( properties.Schema.STRING, _('Reference to a flavor for creating DB instance.'), required=True, update_allowed=True, constraints=[constraints.CustomConstraint('trove.flavor')]), DATASTORE_TYPE: properties.Schema(properties.Schema.STRING, _("Name of registered datastore type."), constraints=[constraints.Length(max=255)]), DATASTORE_VERSION: properties.Schema( properties.Schema.STRING, _("Name of the registered datastore version. " "It must exist for provided datastore type. " "Defaults to using single active version. " "If several active versions exist for provided datastore type, " "explicit value for this parameter must be specified."), constraints=[constraints.Length(max=255)]), SIZE: properties.Schema(properties.Schema.INTEGER, _('Database volume size in GB.'), required=True, update_allowed=True, constraints=[ constraints.Range(1, 150), ]), NICS: properties.Schema( properties.Schema.LIST, _("List of network interfaces to create on instance."), default=[], schema=properties.Schema( properties.Schema.MAP, schema={ NET: properties.Schema( properties.Schema.STRING, _('Name or UUID of the network to attach this NIC to. ' 'Either %(port)s or %(net)s must be specified.') % { 'port': PORT, 'net': NET }, constraints=[ constraints.CustomConstraint('neutron.network') ]), PORT: properties.Schema( properties.Schema.STRING, _('Name or UUID of Neutron port to attach this ' 'NIC to. ' 'Either %(port)s or %(net)s must be specified.') % { 'port': PORT, 'net': NET }, constraints=[ constraints.CustomConstraint('neutron.port') ], ), V4_FIXED_IP: properties.Schema( properties.Schema.STRING, _('Fixed IPv4 address for this NIC.'), constraints=[constraints.CustomConstraint('ip_addr')]), }, ), ), DATABASES: properties.Schema( properties.Schema.LIST, _('List of databases to be created on DB instance creation.'), default=[], update_allowed=True, schema=properties.Schema( properties.Schema.MAP, schema={ DATABASE_CHARACTER_SET: properties.Schema(properties.Schema.STRING, _('Set of symbols and encodings.'), default='utf8'), DATABASE_COLLATE: properties.Schema( properties.Schema.STRING, _('Set of rules for comparing characters in a ' 'character set.'), default='utf8_general_ci'), DATABASE_NAME: properties.Schema( properties.Schema.STRING, _('Specifies database names for creating ' 'databases on instance creation.'), required=True, constraints=[ constraints.Length(max=64), constraints.AllowedPattern(r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ]), }, )), USERS: properties.Schema( properties.Schema.LIST, _('List of users to be created on DB instance creation.'), default=[], update_allowed=True, schema=properties.Schema( properties.Schema.MAP, schema={ USER_NAME: properties.Schema( properties.Schema.STRING, _('User name to create a user on instance ' 'creation.'), required=True, update_allowed=True, constraints=[ constraints.Length(max=16), constraints.AllowedPattern(r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ]), USER_PASSWORD: properties.Schema(properties.Schema.STRING, _('Password for those users on instance ' 'creation.'), required=True, update_allowed=True, constraints=[ constraints.AllowedPattern( r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ]), USER_HOST: properties.Schema( properties.Schema.STRING, _('The host from which a user is allowed to ' 'connect to the database.'), default='%', update_allowed=True), USER_DATABASES: properties.Schema( properties.Schema.LIST, _('Names of databases that those users can ' 'access on instance creation.'), schema=properties.Schema(properties.Schema.STRING, ), required=True, update_allowed=True, constraints=[ constraints.Length(min=1), ]), }, )), AVAILABILITY_ZONE: properties.Schema(properties.Schema.STRING, _('Name of the availability zone for DB instance.')), RESTORE_POINT: properties.Schema(properties.Schema.STRING, _('DB instance restore point.')), REPLICA_OF: properties.Schema( properties.Schema.STRING, _('Identifier of the source instance to replicate.'), support_status=support.SupportStatus(version='5.0.0')), REPLICA_COUNT: properties.Schema( properties.Schema.INTEGER, _('The number of replicas to be created.'), support_status=support.SupportStatus(version='5.0.0')), } attributes_schema = { HOSTNAME: attributes.Schema(_("Hostname of the instance."), type=attributes.Schema.STRING), HREF: attributes.Schema(_("Api endpoint reference of the instance."), type=attributes.Schema.STRING), } default_client_name = 'trove' entity = 'instances' def __init__(self, name, json_snippet, stack): super(OSDBInstance, self).__init__(name, json_snippet, stack) self._href = None self._dbinstance = None @property def dbinstance(self): """Get the trove dbinstance.""" if not self._dbinstance and self.resource_id: self._dbinstance = self.client().instances.get(self.resource_id) return self._dbinstance def _dbinstance_name(self): name = self.properties[self.NAME] if name: return name return self.physical_resource_name() def handle_create(self): """Create cloud database instance.""" self.flavor = self.client_plugin().find_flavor_by_name_or_id( self.properties[self.FLAVOR]) self.volume = {'size': self.properties[self.SIZE]} self.databases = self.properties[self.DATABASES] self.users = self.properties[self.USERS] restore_point = self.properties[self.RESTORE_POINT] if restore_point: restore_point = {"backupRef": restore_point} zone = self.properties[self.AVAILABILITY_ZONE] self.datastore_type = self.properties[self.DATASTORE_TYPE] self.datastore_version = self.properties[self.DATASTORE_VERSION] replica_of = self.properties[self.REPLICA_OF] replica_count = self.properties[self.REPLICA_COUNT] # convert user databases to format required for troveclient. # that is, list of database dictionaries for user in self.users: dbs = [{'name': db} for db in user.get(self.USER_DATABASES, [])] user[self.USER_DATABASES] = dbs # convert networks to format required by troveclient nics = [] for nic in self.properties[self.NICS]: nic_dict = {} net = nic.get(self.NET) if net: if self.is_using_neutron(): net_id = self.client_plugin( 'neutron').find_resourceid_by_name_or_id( 'network', net) else: net_id = ( self.client_plugin('nova').get_nova_network_id(net)) nic_dict['net-id'] = net_id port = nic.get(self.PORT) if port: neutron = self.client_plugin('neutron') nic_dict['port-id'] = neutron.find_resourceid_by_name_or_id( 'port', port) ip = nic.get(self.V4_FIXED_IP) if ip: nic_dict['v4-fixed-ip'] = ip nics.append(nic_dict) # create db instance instance = self.client().instances.create( self._dbinstance_name(), self.flavor, volume=self.volume, databases=self.databases, users=self.users, restorePoint=restore_point, availability_zone=zone, datastore=self.datastore_type, datastore_version=self.datastore_version, nics=nics, replica_of=replica_of, replica_count=replica_count) self.resource_id_set(instance.id) return instance.id def _refresh_instance(self, instance_id): try: instance = self.client().instances.get(instance_id) return instance except Exception as exc: if self.client_plugin().is_over_limit(exc): LOG.warning( _LW("Stack %(name)s (%(id)s) received an " "OverLimit response during instance.get():" " %(exception)s"), { 'name': self.stack.name, 'id': self.stack.id, 'exception': exc }) return None else: raise def check_create_complete(self, instance_id): """Check if cloud DB instance creation is complete.""" instance = self._refresh_instance(instance_id) # refresh attributes if instance is None: return False if instance.status in self.BAD_STATUSES: raise exception.ResourceInError( resource_status=instance.status, status_reason=self.TROVE_STATUS_REASON.get( instance.status, _("Unknown"))) if instance.status != self.ACTIVE: return False LOG.info( _LI("Database instance %(database)s created (flavor:%(" "flavor)s,volume:%(volume)s, datastore:%(" "datastore_type)s, datastore_version:%(" "datastore_version)s)"), { 'database': self._dbinstance_name(), 'flavor': self.flavor, 'volume': self.volume, 'datastore_type': self.datastore_type, 'datastore_version': self.datastore_version }) return True def handle_check(self): instance = self.client().instances.get(self.resource_id) status = instance.status checks = [ { 'attr': 'status', 'expected': self.ACTIVE, 'current': status }, ] self._verify_check_conditions(checks) def handle_update(self, json_snippet, tmpl_diff, prop_diff): updates = {} if prop_diff: instance = self.client().instances.get(self.resource_id) if self.NAME in prop_diff: updates.update({self.NAME: prop_diff[self.NAME]}) if self.FLAVOR in prop_diff: flvid = prop_diff[self.FLAVOR] flv = self.client_plugin().get_flavor_id(flvid) updates.update({self.FLAVOR: flv}) if self.SIZE in prop_diff: updates.update({self.SIZE: prop_diff[self.SIZE]}) if self.DATABASES in prop_diff: current = [ d.name for d in self.client().databases.list(instance) ] desired = [ d[self.DATABASE_NAME] for d in prop_diff[self.DATABASES] ] for db in prop_diff[self.DATABASES]: dbname = db[self.DATABASE_NAME] if dbname not in current: db['ACTION'] = self.CREATE for dbname in current: if dbname not in desired: deleted = { self.DATABASE_NAME: dbname, 'ACTION': self.DELETE } prop_diff[self.DATABASES].append(deleted) updates.update({self.DATABASES: prop_diff[self.DATABASES]}) if self.USERS in prop_diff: current = [u.name for u in self.client().users.list(instance)] desired = [u[self.USER_NAME] for u in prop_diff[self.USERS]] for usr in prop_diff[self.USERS]: if usr[self.USER_NAME] not in current: usr['ACTION'] = self.CREATE for usr in current: if usr not in desired: prop_diff[self.USERS].append({ self.USER_NAME: usr, 'ACTION': self.DELETE }) updates.update({self.USERS: prop_diff[self.USERS]}) return updates def check_update_complete(self, updates): instance = self.client().instances.get(self.resource_id) if instance.status in self.BAD_STATUSES: raise exception.ResourceInError( resource_status=instance.status, status_reason=self.TROVE_STATUS_REASON.get( instance.status, _("Unknown"))) if updates: if instance.status != self.ACTIVE: dmsg = ("Instance is in status %(now)s. Waiting on status" " %(stat)s") LOG.debug(dmsg % {"now": instance.status, "stat": self.ACTIVE}) return False try: return ( self._update_name(instance, updates.get(self.NAME)) and self._update_flavor(instance, updates.get(self.FLAVOR)) and self._update_size(instance, updates.get(self.SIZE)) and self._update_databases(instance, updates.get(self.DATABASES)) and self._update_users(instance, updates.get(self.USERS))) except Exception as exc: if self.client_plugin().is_client_exception(exc): # the instance could have updated between the time # we retrieve it and try to update it so check again if self.client_plugin().is_over_limit(exc): LOG.debug("API rate limit: %(ex)s. Retrying." % {'ex': six.text_type(exc)}) return False if "No change was requested" in six.text_type(exc): LOG.warning( _LW("Unexpected instance state change " "during update. Retrying.")) return False raise return True def _update_name(self, instance, name): if name and instance.name != name: self.client().instances.edit(instance, name=name) return False return True def _update_flavor(self, instance, new_flavor): if new_flavor: current_flav = six.text_type(instance.flavor['id']) new_flav = six.text_type(new_flavor) if new_flav != current_flav: dmsg = "Resizing instance flavor from %(old)s to %(new)s" LOG.debug(dmsg % {"old": current_flav, "new": new_flav}) self.client().instances.resize_instance(instance, new_flavor) return False return True def _update_size(self, instance, new_size): if new_size and instance.volume['size'] != new_size: dmsg = "Resizing instance storage from %(old)s to %(new)s" LOG.debug(dmsg % {"old": instance.volume['size'], "new": new_size}) self.client().instances.resize_volume(instance, new_size) return False return True def _update_databases(self, instance, databases): if databases: for db in databases: if db.get("ACTION") == self.CREATE: db.pop("ACTION", None) dmsg = "Adding new database %(db)s to instance" LOG.debug(dmsg % {"db": db}) self.client().databases.create(instance, [db]) elif db.get("ACTION") == self.DELETE: dmsg = ("Deleting existing database %(db)s from " "instance") LOG.debug(dmsg % {"db": db['name']}) self.client().databases.delete(instance, db['name']) return True def _update_users(self, instance, users): if users: for usr in users: dbs = [{'name': db} for db in usr.get(self.USER_DATABASES, [])] usr[self.USER_DATABASES] = dbs if usr.get("ACTION") == self.CREATE: usr.pop("ACTION", None) dmsg = "Adding new user %(u)s to instance" LOG.debug(dmsg % {"u": usr}) self.client().users.create(instance, [usr]) elif usr.get("ACTION") == self.DELETE: dmsg = ("Deleting existing user %(u)s from " "instance") LOG.debug(dmsg % {"u": usr['name']}) self.client().users.delete(instance, usr['name']) else: newattrs = {} if usr.get(self.USER_HOST): newattrs[self.USER_HOST] = usr[self.USER_HOST] if usr.get(self.USER_PASSWORD): newattrs[self.USER_PASSWORD] = usr[self.USER_PASSWORD] if newattrs: self.client().users.update_attributes( instance, usr['name'], newuserattr=newattrs, hostname=instance.hostname) current = self.client().users.get(instance, usr[self.USER_NAME]) dbs = [db['name'] for db in current.databases] desired = [ db['name'] for db in usr.get(self.USER_DATABASES, []) ] grants = [db for db in desired if db not in dbs] revokes = [db for db in dbs if db not in desired] if grants: self.client().users.grant(instance, usr[self.USER_NAME], grants) if revokes: self.client().users.revoke(instance, usr[self.USER_NAME], revokes) return True def handle_delete(self): """Delete a cloud database instance.""" if not self.resource_id: return try: instance = self.client().instances.get(self.resource_id) except Exception as ex: self.client_plugin().ignore_not_found(ex) else: instance.delete() return instance.id def check_delete_complete(self, instance_id): """Check for completion of cloud DB instance deletion.""" if not instance_id: return True try: # For some time trove instance may continue to live self._refresh_instance(instance_id) except Exception as ex: self.client_plugin().ignore_not_found(ex) return True return False def validate(self): """Validate any of the provided params.""" res = super(OSDBInstance, self).validate() if res: return res datastore_type = self.properties[self.DATASTORE_TYPE] datastore_version = self.properties[self.DATASTORE_VERSION] self.client_plugin().validate_datastore(datastore_type, datastore_version, self.DATASTORE_TYPE, self.DATASTORE_VERSION) # check validity of user and databases users = self.properties[self.USERS] if users: databases = self.properties[self.DATABASES] if not databases: msg = _('Databases property is required if users property ' 'is provided for resource %s.') % self.name raise exception.StackValidationFailed(message=msg) db_names = set([db[self.DATABASE_NAME] for db in databases]) for user in users: missing_db = [ db_name for db_name in user[self.USER_DATABASES] if db_name not in db_names ] if missing_db: msg = (_('Database %(dbs)s specified for user does ' 'not exist in databases for resource %(name)s.') % { 'dbs': missing_db, 'name': self.name }) raise exception.StackValidationFailed(message=msg) # check validity of NICS is_neutron = self.is_using_neutron() nics = self.properties[self.NICS] for nic in nics: if not is_neutron and nic.get(self.PORT): msg = _("Can not use %s property on Nova-network.") % self.PORT raise exception.StackValidationFailed(message=msg) if bool(nic.get(self.NET)) == bool(nic.get(self.PORT)): msg = _("Either %(net)s or %(port)s must be provided.") % { 'net': self.NET, 'port': self.PORT } raise exception.StackValidationFailed(message=msg) def href(self): if not self._href and self.dbinstance: if not self.dbinstance.links: self._href = None else: for link in self.dbinstance.links: if link['rel'] == 'self': self._href = link[self.HREF] break return self._href def _resolve_attribute(self, name): if name == self.HOSTNAME: return self.dbinstance.hostname elif name == self.HREF: return self.href()
class CloudNetwork(resource.Resource): """A resource for creating Rackspace Cloud Networks. See http://www.rackspace.com/cloud/networks/ for service documentation. """ support_status = support.SupportStatus( status=support.DEPRECATED, message=_('Use OS::Neutron::Net instead.'), version='2015.1', previous_status=support.SupportStatus(version='2014.1')) PROPERTIES = (LABEL, CIDR) = ("label", "cidr") ATTRIBUTES = ( CIDR_ATTR, LABEL_ATTR, ) = ( 'cidr', 'label', ) properties_schema = { LABEL: properties.Schema(properties.Schema.STRING, _("The name of the network."), required=True, constraints=[constraints.Length(min=3, max=64)]), CIDR: properties.Schema( properties.Schema.STRING, _("The IP block from which to allocate the network. For example, " "172.16.0.0/24 or 2001:DB8::/64."), required=True, constraints=[constraints.CustomConstraint('net_cidr')]) } attributes_schema = { CIDR_ATTR: attributes.Schema(_("The CIDR for an isolated private network.")), LABEL_ATTR: attributes.Schema(_("The name of the network.")), } def __init__(self, name, json_snippet, stack): resource.Resource.__init__(self, name, json_snippet, stack) self._network = None self._delete_issued = False def network(self): if self.resource_id and not self._network: try: self._network = self.cloud_networks().get(self.resource_id) except NotFound: LOG.warn( _LW("Could not find network %s but resource id is" " set."), self.resource_id) return self._network def cloud_networks(self): return self.client('cloud_networks') def handle_create(self): cnw = self.cloud_networks().create(label=self.properties[self.LABEL], cidr=self.properties[self.CIDR]) self.resource_id_set(cnw.id) def handle_check(self): self.cloud_networks().get(self.resource_id) def check_delete_complete(self, cookie): try: network = self.cloud_networks().get(self.resource_id) except NotFound: return True if not network: return True if not self._delete_issued: try: network.delete() except NetworkInUse: LOG.warn(_LW("Network '%s' still in use."), network.id) else: self._delete_issued = True return False return False def validate(self): super(CloudNetwork, self).validate() def _resolve_attribute(self, name): net = self.network() if net: return six.text_type(getattr(net, name)) return ""
class SaharaNodeGroupTemplate(resource.Resource): support_status = support.SupportStatus(version='2014.2') PROPERTIES = (NAME, PLUGIN_NAME, HADOOP_VERSION, FLAVOR, DESCRIPTION, VOLUMES_PER_NODE, VOLUMES_SIZE, VOLUME_TYPE, SECURITY_GROUPS, AUTO_SECURITY_GROUP, AVAILABILITY_ZONE, VOLUMES_AVAILABILITY_ZONE, NODE_PROCESSES, FLOATING_IP_POOL, NODE_CONFIGS, IMAGE_ID, IS_PROXY_GATEWAY, VOLUME_LOCAL_TO_INSTANCE, USE_AUTOCONFIG) = ( 'name', 'plugin_name', 'hadoop_version', 'flavor', 'description', 'volumes_per_node', 'volumes_size', 'volume_type', 'security_groups', 'auto_security_group', 'availability_zone', 'volumes_availability_zone', 'node_processes', 'floating_ip_pool', 'node_configs', 'image_id', 'is_proxy_gateway', 'volume_local_to_instance', 'use_autoconfig') properties_schema = { NAME: properties.Schema( properties.Schema.STRING, _("Name for the Sahara Node Group Template."), constraints=[ constraints.Length(min=1, max=50), constraints.AllowedPattern(SAHARA_NAME_REGEX), ], ), DESCRIPTION: properties.Schema( properties.Schema.STRING, _('Description of the Node Group Template.'), default="", ), PLUGIN_NAME: properties.Schema( properties.Schema.STRING, _('Plugin name.'), required=True, ), HADOOP_VERSION: properties.Schema( properties.Schema.STRING, _('Version of Hadoop running on instances.'), required=True, ), FLAVOR: properties.Schema( properties.Schema.STRING, _('Name or ID Nova flavor for the nodes.'), required=True, constraints=[constraints.CustomConstraint('nova.flavor')]), VOLUMES_PER_NODE: properties.Schema( properties.Schema.INTEGER, _("Volumes per node."), constraints=[ constraints.Range(min=0), ], ), VOLUMES_SIZE: properties.Schema( properties.Schema.INTEGER, _("Size of the volumes, in GB."), constraints=[ constraints.Range(min=1), ], ), VOLUME_TYPE: properties.Schema( properties.Schema.STRING, _("Type of the volume to create on Cinder backend."), constraints=[constraints.CustomConstraint('cinder.vtype')]), SECURITY_GROUPS: properties.Schema( properties.Schema.LIST, _("List of security group names or IDs to assign to this " "Node Group template."), schema=properties.Schema(properties.Schema.STRING, ), ), AUTO_SECURITY_GROUP: properties.Schema( properties.Schema.BOOLEAN, _("Defines whether auto-assign security group to this " "Node Group template."), ), AVAILABILITY_ZONE: properties.Schema( properties.Schema.STRING, _("Availability zone to create servers in."), ), VOLUMES_AVAILABILITY_ZONE: properties.Schema( properties.Schema.STRING, _("Availability zone to create volumes in."), ), NODE_PROCESSES: properties.Schema( properties.Schema.LIST, _("List of processes to run on every node."), required=True, constraints=[ constraints.Length(min=1), ], schema=properties.Schema(properties.Schema.STRING, ), ), FLOATING_IP_POOL: properties.Schema( properties.Schema.STRING, _("Name or UUID of the Neutron floating IP network or " "name of the Nova floating ip pool to use. " "Should not be provided when used with Nova-network " "that auto-assign floating IPs."), ), NODE_CONFIGS: properties.Schema( properties.Schema.MAP, _("Dictionary of node configurations."), ), IMAGE_ID: properties.Schema( properties.Schema.STRING, _("ID of the image to use for the template."), constraints=[ constraints.CustomConstraint('sahara.image'), ], ), IS_PROXY_GATEWAY: properties.Schema( properties.Schema.BOOLEAN, _("Provide access to nodes using other nodes of the cluster " "as proxy gateways."), support_status=support.SupportStatus(version='5.0.0')), VOLUME_LOCAL_TO_INSTANCE: properties.Schema( properties.Schema.BOOLEAN, _("Create volumes on the same physical port as an instance."), support_status=support.SupportStatus(version='5.0.0')), USE_AUTOCONFIG: properties.Schema( properties.Schema.BOOLEAN, _("Configure most important configs automatically."), support_status=support.SupportStatus(version='5.0.0')) } default_client_name = 'sahara' physical_resource_name_limit = 50 entity = 'node_group_templates' def _ngt_name(self): name = self.properties[self.NAME] if name: return name return re.sub('[^a-zA-Z0-9-]', '', self.physical_resource_name()) def handle_create(self): plugin_name = self.properties[self.PLUGIN_NAME] hadoop_version = self.properties[self.HADOOP_VERSION] node_processes = self.properties[self.NODE_PROCESSES] description = self.properties[self.DESCRIPTION] flavor_id = self.client_plugin("nova").get_flavor_id( self.properties[self.FLAVOR]) volumes_per_node = self.properties[self.VOLUMES_PER_NODE] volumes_size = self.properties[self.VOLUMES_SIZE] volume_type = self.properties[self.VOLUME_TYPE] floating_ip_pool = self.properties[self.FLOATING_IP_POOL] security_groups = self.properties[self.SECURITY_GROUPS] auto_security_group = self.properties[self.AUTO_SECURITY_GROUP] availability_zone = self.properties[self.AVAILABILITY_ZONE] vol_availability_zone = self.properties[self.VOLUMES_AVAILABILITY_ZONE] image_id = self.properties[self.IMAGE_ID] if floating_ip_pool and self.is_using_neutron(): floating_ip_pool = self.client_plugin( 'neutron').find_neutron_resource(self.properties, self.FLOATING_IP_POOL, 'network') node_configs = self.properties[self.NODE_CONFIGS] is_proxy_gateway = self.properties[self.IS_PROXY_GATEWAY] volume_local_to_instance = self.properties[ self.VOLUME_LOCAL_TO_INSTANCE] use_autoconfig = self.properties[self.USE_AUTOCONFIG] node_group_template = self.client().node_group_templates.create( self._ngt_name(), plugin_name, hadoop_version, flavor_id, description=description, volumes_per_node=volumes_per_node, volumes_size=volumes_size, volume_type=volume_type, node_processes=node_processes, floating_ip_pool=floating_ip_pool, node_configs=node_configs, security_groups=security_groups, auto_security_group=auto_security_group, availability_zone=availability_zone, volumes_availability_zone=vol_availability_zone, image_id=image_id, is_proxy_gateway=is_proxy_gateway, volume_local_to_instance=volume_local_to_instance, use_autoconfig=use_autoconfig) LOG.info(_LI("Node Group Template '%s' has been created"), node_group_template.name) self.resource_id_set(node_group_template.id) return self.resource_id def validate(self): res = super(SaharaNodeGroupTemplate, self).validate() if res: return res pool = self.properties[self.FLOATING_IP_POOL] if pool: if self.is_using_neutron(): try: self.client_plugin('neutron').find_neutron_resource( self.properties, self.FLOATING_IP_POOL, 'network') except Exception as ex: if (self.client_plugin('neutron').is_not_found(ex) or self.client_plugin('neutron').is_no_unique(ex)): raise exception.StackValidationFailed( message=ex.message) raise else: try: self.client('nova').floating_ip_pools.find(name=pool) except Exception as ex: if self.client_plugin('nova').is_not_found(ex): raise exception.StackValidationFailed( message=ex.message) raise
class SubnetPool(neutron.NeutronResource): """A resource that implements neutron subnet pool. This resource can be used to create a subnet pool with a large block of addresses and create subnets from it. """ support_status = support.SupportStatus(version='6.0.0') required_service_extension = 'subnet_allocation' entity = 'subnetpool' PROPERTIES = ( NAME, PREFIXES, ADDRESS_SCOPE, DEFAULT_QUOTA, DEFAULT_PREFIXLEN, MIN_PREFIXLEN, MAX_PREFIXLEN, IS_DEFAULT, TENANT_ID, SHARED, TAGS, ) = ( 'name', 'prefixes', 'address_scope', 'default_quota', 'default_prefixlen', 'min_prefixlen', 'max_prefixlen', 'is_default', 'tenant_id', 'shared', 'tags', ) properties_schema = { NAME: properties.Schema(properties.Schema.STRING, _('Name of the subnet pool.'), update_allowed=True), PREFIXES: properties.Schema( properties.Schema.LIST, _('List of subnet prefixes to assign.'), schema=properties.Schema( properties.Schema.STRING, constraints=[ constraints.CustomConstraint('net_cidr'), ], ), constraints=[constraints.Length(min=1)], required=True, update_allowed=True, ), ADDRESS_SCOPE: properties.Schema( properties.Schema.STRING, _('An address scope ID to assign to the subnet pool.'), constraints=[ constraints.CustomConstraint('neutron.address_scope') ], update_allowed=True, ), DEFAULT_QUOTA: properties.Schema( properties.Schema.INTEGER, _('A per-tenant quota on the prefix space that can be allocated ' 'from the subnet pool for tenant subnets.'), constraints=[constraints.Range(min=0)], update_allowed=True, ), DEFAULT_PREFIXLEN: properties.Schema( properties.Schema.INTEGER, _('The size of the prefix to allocate when the cidr or ' 'prefixlen attributes are not specified while creating ' 'a subnet.'), constraints=[constraints.Range(min=0)], update_allowed=True, ), MIN_PREFIXLEN: properties.Schema( properties.Schema.INTEGER, _('Smallest prefix size that can be allocated ' 'from the subnet pool.'), constraints=[constraints.Range(min=0)], update_allowed=True, ), MAX_PREFIXLEN: properties.Schema( properties.Schema.INTEGER, _('Maximum prefix size that can be allocated ' 'from the subnet pool.'), constraints=[constraints.Range(min=0)], update_allowed=True, ), IS_DEFAULT: properties.Schema( properties.Schema.BOOLEAN, _('Whether this is default IPv4/IPv6 subnet pool. ' 'There can only be one default subnet pool for each IP family. ' 'Note that the default policy setting restricts administrative ' 'users to set this to True.'), default=False, update_allowed=True, ), TENANT_ID: properties.Schema( properties.Schema.STRING, _('The ID of the tenant who owns the subnet pool. Only ' 'administrative users can specify a tenant ID ' 'other than their own.')), SHARED: properties.Schema( properties.Schema.BOOLEAN, _('Whether the subnet pool will be shared across all tenants. ' 'Note that the default policy setting restricts usage of this ' 'attribute to administrative users only.'), default=False, ), TAGS: properties.Schema( properties.Schema.LIST, _('The tags to be added to the subnetpool.'), schema=properties.Schema(properties.Schema.STRING), update_allowed=True, support_status=support.SupportStatus(version='9.0.0')), } def validate(self): super(SubnetPool, self).validate() self._validate_prefix_bounds() def _validate_prefix_bounds(self): min_prefixlen = self.properties[self.MIN_PREFIXLEN] default_prefixlen = self.properties[self.DEFAULT_PREFIXLEN] max_prefixlen = self.properties[self.MAX_PREFIXLEN] msg_fmt = _('Illegal prefix bounds: %(key1)s=%(value1)s, ' '%(key2)s=%(value2)s.') # min_prefixlen can not be greater than max_prefixlen if min_prefixlen and max_prefixlen and min_prefixlen > max_prefixlen: msg = msg_fmt % dict(key1=self.MAX_PREFIXLEN, value1=max_prefixlen, key2=self.MIN_PREFIXLEN, value2=min_prefixlen) raise exception.StackValidationFailed(message=msg) if default_prefixlen: # default_prefixlen can not be greater than max_prefixlen if max_prefixlen and default_prefixlen > max_prefixlen: msg = msg_fmt % dict(key1=self.MAX_PREFIXLEN, value1=max_prefixlen, key2=self.DEFAULT_PREFIXLEN, value2=default_prefixlen) raise exception.StackValidationFailed(message=msg) # min_prefixlen can not be greater than default_prefixlen if min_prefixlen and min_prefixlen > default_prefixlen: msg = msg_fmt % dict(key1=self.MIN_PREFIXLEN, value1=min_prefixlen, key2=self.DEFAULT_PREFIXLEN, value2=default_prefixlen) raise exception.StackValidationFailed(message=msg) def _validate_prefixes_for_update(self, prop_diff): old_prefixes = self.properties[self.PREFIXES] new_prefixes = prop_diff[self.PREFIXES] # check new_prefixes is a superset of old_prefixes if not netutils.is_prefix_subset(old_prefixes, new_prefixes): msg = (_('Property %(key)s updated value %(new)s should ' 'be superset of existing value ' '%(old)s.') % dict(key=self.PREFIXES, new=sorted(new_prefixes), old=sorted(old_prefixes))) raise exception.StackValidationFailed(message=msg) def handle_create(self): props = self.prepare_properties(self.properties, self.physical_resource_name()) if self.ADDRESS_SCOPE in props and props[self.ADDRESS_SCOPE]: client_plugin = self.client_plugin() scope_id = client_plugin.find_resourceid_by_name_or_id( client_plugin.RES_TYPE_ADDRESS_SCOPE, props.pop(self.ADDRESS_SCOPE)) props['address_scope_id'] = scope_id tags = props.pop(self.TAGS, []) subnetpool = self.client().create_subnetpool({'subnetpool': props})['subnetpool'] self.resource_id_set(subnetpool['id']) if tags: self.set_tags(tags) def handle_delete(self): if self.resource_id is not None: with self.client_plugin().ignore_not_found: self.client().delete_subnetpool(self.resource_id) def handle_update(self, json_snippet, tmpl_diff, prop_diff): # check that new prefixes are superset of existing prefixes if self.PREFIXES in prop_diff: self._validate_prefixes_for_update(prop_diff) if self.ADDRESS_SCOPE in prop_diff: if prop_diff[self.ADDRESS_SCOPE]: client_plugin = self.client_plugin() scope_id = client_plugin.find_resourceid_by_name_or_id( self.client(), client_plugin.RES_TYPE_ADDRESS_SCOPE, prop_diff.pop(self.ADDRESS_SCOPE)) else: scope_id = prop_diff.pop(self.ADDRESS_SCOPE) prop_diff['address_scope_id'] = scope_id if self.TAGS in prop_diff: tags = prop_diff.pop(self.TAGS) self.set_tags(tags) if prop_diff: self.prepare_update_properties(prop_diff) self.client().update_subnetpool(self.resource_id, {'subnetpool': prop_diff})
class SaharaClusterTemplate(resource.Resource): support_status = support.SupportStatus(version='2014.2') PROPERTIES = ( NAME, PLUGIN_NAME, HADOOP_VERSION, DESCRIPTION, ANTI_AFFINITY, MANAGEMENT_NETWORK, CLUSTER_CONFIGS, NODE_GROUPS, IMAGE_ID, ) = ( 'name', 'plugin_name', 'hadoop_version', 'description', 'anti_affinity', 'neutron_management_network', 'cluster_configs', 'node_groups', 'default_image_id', ) _NODE_GROUP_KEYS = ( NG_NAME, COUNT, NG_TEMPLATE_ID, ) = ( 'name', 'count', 'node_group_template_id', ) properties_schema = { NAME: properties.Schema( properties.Schema.STRING, _("Name for the Sahara Cluster Template."), constraints=[ constraints.Length(min=1, max=50), constraints.AllowedPattern(SAHARA_NAME_REGEX), ], ), DESCRIPTION: properties.Schema( properties.Schema.STRING, _('Description of the Sahara Group Template.'), default="", ), PLUGIN_NAME: properties.Schema( properties.Schema.STRING, _('Plugin name.'), required=True, ), HADOOP_VERSION: properties.Schema( properties.Schema.STRING, _('Version of Hadoop running on instances.'), required=True, ), IMAGE_ID: properties.Schema( properties.Schema.STRING, _("ID of the default image to use for the template."), constraints=[ constraints.CustomConstraint('sahara.image'), ], ), MANAGEMENT_NETWORK: properties.Schema( properties.Schema.STRING, _('Name or UUID of network.'), constraints=[constraints.CustomConstraint('neutron.network')], ), ANTI_AFFINITY: properties.Schema( properties.Schema.LIST, _("List of processes to enable anti-affinity for."), schema=properties.Schema(properties.Schema.STRING, ), ), CLUSTER_CONFIGS: properties.Schema( properties.Schema.MAP, _('Cluster configs dictionary.'), ), NODE_GROUPS: properties.Schema( properties.Schema.LIST, _('Node groups.'), schema=properties.Schema( properties.Schema.MAP, schema={ NG_NAME: properties.Schema(properties.Schema.STRING, _('Name of the Node group.'), required=True), COUNT: properties.Schema( properties.Schema.INTEGER, _("Number of instances in the Node group."), required=True, constraints=[constraints.Range(min=1)]), NG_TEMPLATE_ID: properties.Schema(properties.Schema.STRING, _("ID of the Node Group Template."), required=True), }), ), } default_client_name = 'sahara' physical_resource_name_limit = 50 entity = 'cluster_templates' def _cluster_template_name(self): name = self.properties[self.NAME] if name: return name return re.sub('[^a-zA-Z0-9-]', '', self.physical_resource_name()) def handle_create(self): plugin_name = self.properties[self.PLUGIN_NAME] hadoop_version = self.properties[self.HADOOP_VERSION] description = self.properties[self.DESCRIPTION] image_id = self.properties[self.IMAGE_ID] net_id = self.properties[self.MANAGEMENT_NETWORK] if net_id: if self.is_using_neutron(): net_id = self.client_plugin('neutron').find_neutron_resource( self.properties, self.MANAGEMENT_NETWORK, 'network') else: net_id = self.client_plugin('nova').get_nova_network_id(net_id) anti_affinity = self.properties[self.ANTI_AFFINITY] cluster_configs = self.properties[self.CLUSTER_CONFIGS] node_groups = self.properties[self.NODE_GROUPS] cluster_template = self.client().cluster_templates.create( self._cluster_template_name(), plugin_name, hadoop_version, description=description, default_image_id=image_id, anti_affinity=anti_affinity, net_id=net_id, cluster_configs=cluster_configs, node_groups=node_groups) LOG.info(_LI("Cluster Template '%s' has been created"), cluster_template.name) self.resource_id_set(cluster_template.id) return self.resource_id def validate(self): res = super(SaharaClusterTemplate, self).validate() if res: return res # check if running on neutron and MANAGEMENT_NETWORK missing if (self.is_using_neutron() and not self.properties[self.MANAGEMENT_NETWORK]): msg = _("%s must be provided") % self.MANAGEMENT_NETWORK raise exception.StackValidationFailed(message=msg)
class KeyPair(resource.Resource): """A resource for creating Nova key pairs. A keypair is a ssh key that can be injected into a server on launch. **Note** that if a new key is generated setting `save_private_key` to `True` results in the system saving the private key which can then be retrieved via the `private_key` attribute of this resource. Setting the `public_key` property means that the `private_key` attribute of this resource will always return an empty string regardless of the `save_private_key` setting since there will be no private key data to save. """ support_status = support.SupportStatus(version='2014.1') required_service_extension = 'os-keypairs' PROPERTIES = ( NAME, SAVE_PRIVATE_KEY, PUBLIC_KEY, ) = ( 'name', 'save_private_key', 'public_key', ) ATTRIBUTES = ( PUBLIC_KEY_ATTR, PRIVATE_KEY_ATTR, ) = ( 'public_key', 'private_key', ) properties_schema = { NAME: properties.Schema( properties.Schema.STRING, _('The name of the key pair.'), required=True, constraints=[ constraints.Length(min=1, max=255) ] ), SAVE_PRIVATE_KEY: properties.Schema( properties.Schema.BOOLEAN, _('True if the system should remember a generated private key; ' 'False otherwise.'), default=False ), PUBLIC_KEY: properties.Schema( properties.Schema.STRING, _('The optional public key. This allows users to supply the ' 'public key from a pre-existing key pair. If not supplied, a ' 'new key pair will be generated.') ), } attributes_schema = { PUBLIC_KEY_ATTR: attributes.Schema( _('The public key.'), type=attributes.Schema.STRING ), PRIVATE_KEY_ATTR: attributes.Schema( _('The private key if it has been saved.'), cache_mode=attributes.Schema.CACHE_NONE, type=attributes.Schema.STRING ), } default_client_name = 'nova' entity = 'keypairs' def __init__(self, name, json_snippet, stack): super(KeyPair, self).__init__(name, json_snippet, stack) self._public_key = None @property def private_key(self): """Return the private SSH key for the resource.""" if self.properties[self.SAVE_PRIVATE_KEY]: return self.data().get('private_key', '') else: return '' @property def public_key(self): """Return the public SSH key for the resource.""" if not self._public_key: if self.properties[self.PUBLIC_KEY]: self._public_key = self.properties[self.PUBLIC_KEY] elif self.resource_id: nova_key = self.client_plugin().get_keypair(self.resource_id) self._public_key = nova_key.public_key return self._public_key def handle_create(self): pub_key = self.properties[self.PUBLIC_KEY] or None new_keypair = self.client().keypairs.create(self.properties[self.NAME], public_key=pub_key) if (self.properties[self.SAVE_PRIVATE_KEY] and hasattr(new_keypair, 'private_key')): self.data_set('private_key', new_keypair.private_key, True) self.resource_id_set(new_keypair.id) def handle_check(self): self.client().keypairs.get(self.resource_id) def _resolve_attribute(self, key): attr_fn = {self.PRIVATE_KEY_ATTR: self.private_key, self.PUBLIC_KEY_ATTR: self.public_key} return six.text_type(attr_fn[key]) def get_reference_id(self): return self.resource_id
class MonascaNotification(resource.Resource): """Heat Template Resource for Monasca Notification. A resource which is used to notificate if there is some alarm. Monasca Notification helps to declare the hook points, which will be invoked once alarm is generated. This plugin helps to create, update and delete the notification. """ support_status = support.SupportStatus( version='7.0.0', previous_status=support.SupportStatus( version='5.0.0', status=support.UNSUPPORTED )) default_client_name = 'monasca' entity = 'notifications' # NOTE(sirushti): To conform to the autoscaling behaviour in heat, we set # the default period interval during create/update to 60 for webhooks only. _default_period_interval = 60 NOTIFICATION_TYPES = ( EMAIL, WEBHOOK, PAGERDUTY ) = ( 'email', 'webhook', 'pagerduty' ) PROPERTIES = ( NAME, TYPE, ADDRESS, PERIOD ) = ( 'name', 'type', 'address', 'period' ) properties_schema = { NAME: properties.Schema( properties.Schema.STRING, _('Name of the notification. By default, physical resource name ' 'is used.'), update_allowed=True ), TYPE: properties.Schema( properties.Schema.STRING, _('Type of the notification.'), update_allowed=True, required=True, constraints=[constraints.AllowedValues( NOTIFICATION_TYPES )] ), ADDRESS: properties.Schema( properties.Schema.STRING, _('Address of the notification. It could be a valid email ' 'address, url or service key based on notification type.'), update_allowed=True, required=True, constraints=[constraints.Length(max=512)] ), PERIOD: properties.Schema( properties.Schema.INTEGER, _('Interval in seconds to invoke webhooks if the alarm state ' 'does not transition away from the defined trigger state. A ' 'value of 0 will disable continuous notifications. This ' 'property is only applicable for the webhook notification ' 'type and has default period interval of 60 seconds.'), support_status=support.SupportStatus(version='7.0.0'), update_allowed=True, constraints=[constraints.AllowedValues([0, 60])] ) } def _period_interval(self): period = self.properties[self.PERIOD] if period is None: period = self._default_period_interval return period def validate(self): super(MonascaNotification, self).validate() if self.properties[self.PERIOD] is not None and ( self.properties[self.TYPE] != self.WEBHOOK): msg = _('The period property can only be specified against a ' 'Webhook Notification type.') raise exception.StackValidationFailed(message=msg) address = self.properties[self.ADDRESS] if not address: return if self.properties[self.TYPE] == self.WEBHOOK: try: parsed_address = urllib.parse.urlparse(address) except Exception: msg = _('Address "%(addr)s" should have correct format ' 'required by "%(wh)s" type of "%(type)s" ' 'property') % { 'addr': address, 'wh': self.WEBHOOK, 'type': self.TYPE} raise exception.StackValidationFailed(message=msg) if not parsed_address.scheme: msg = _('Address "%s" doesn\'t have required URL ' 'scheme') % address raise exception.StackValidationFailed(message=msg) if not parsed_address.netloc: msg = _('Address "%s" doesn\'t have required network ' 'location') % address raise exception.StackValidationFailed(message=msg) if parsed_address.scheme not in ['http', 'https']: msg = _('Address "%(addr)s" doesn\'t satisfies ' 'allowed schemes: %(schemes)s') % { 'addr': address, 'schemes': ', '.join(['http', 'https']) } raise exception.StackValidationFailed(message=msg) elif (self.properties[self.TYPE] == self.EMAIL and not re.match('^\S+@\S+$', address)): msg = _('Address "%(addr)s" doesn\'t satisfies allowed format for ' '"%(email)s" type of "%(type)s" property') % { 'addr': address, 'email': self.EMAIL, 'type': self.TYPE} raise exception.StackValidationFailed(message=msg) def handle_create(self): args = dict( name=(self.properties[self.NAME] or self.physical_resource_name()), type=self.properties[self.TYPE], address=self.properties[self.ADDRESS], ) if args['type'] == self.WEBHOOK: args['period'] = self._period_interval() notification = self.client().notifications.create(**args) self.resource_id_set(notification['id']) def handle_update(self, json_snippet, tmpl_diff, prop_diff): args = dict(notification_id=self.resource_id) args['name'] = (prop_diff.get(self.NAME) or self.properties[self.NAME]) args['type'] = (prop_diff.get(self.TYPE) or self.properties[self.TYPE]) args['address'] = (prop_diff.get(self.ADDRESS) or self.properties[self.ADDRESS]) if args['type'] == self.WEBHOOK: updated_period = prop_diff.get(self.PERIOD) args['period'] = (updated_period if updated_period is not None else self._period_interval()) self.client().notifications.update(**args) def handle_delete(self): if self.resource_id is not None: with self.client_plugin().ignore_not_found: self.client().notifications.delete( notification_id=self.resource_id) # FIXME(kanagaraj-manickam) Remove this method once monasca defect 1484900 # is fixed. def _show_resource(self): return self.client().notifications.get(self.resource_id)
class SaharaClusterTemplate(resource.Resource): support_status = support.SupportStatus(version='2014.2') PROPERTIES = ( NAME, PLUGIN_NAME, HADOOP_VERSION, DESCRIPTION, ANTI_AFFINITY, MANAGEMENT_NETWORK, CLUSTER_CONFIGS, NODE_GROUPS, IMAGE_ID, USE_AUTOCONFIG, SHARES ) = ( 'name', 'plugin_name', 'hadoop_version', 'description', 'anti_affinity', 'neutron_management_network', 'cluster_configs', 'node_groups', 'default_image_id', 'use_autoconfig', 'shares' ) _NODE_GROUP_KEYS = ( NG_NAME, COUNT, NG_TEMPLATE_ID, ) = ( 'name', 'count', 'node_group_template_id', ) _SHARE_KEYS = ( SHARE_ID, PATH, ACCESS_LEVEL ) = ( 'id', 'path', 'access_level' ) properties_schema = { NAME: properties.Schema( properties.Schema.STRING, _("Name for the Sahara Cluster Template."), constraints=[ constraints.Length(min=1, max=50), constraints.AllowedPattern(SAHARA_NAME_REGEX), ], update_allowed=True ), DESCRIPTION: properties.Schema( properties.Schema.STRING, _('Description of the Sahara Group Template.'), default="", update_allowed=True ), PLUGIN_NAME: properties.Schema( properties.Schema.STRING, _('Plugin name.'), required=True, constraints=[ constraints.CustomConstraint('sahara.plugin') ], update_allowed=True ), HADOOP_VERSION: properties.Schema( properties.Schema.STRING, _('Version of Hadoop running on instances.'), required=True, update_allowed=True ), IMAGE_ID: properties.Schema( properties.Schema.STRING, _("ID of the default image to use for the template."), constraints=[ constraints.CustomConstraint('sahara.image'), ], update_allowed=True ), MANAGEMENT_NETWORK: properties.Schema( properties.Schema.STRING, _('Name or UUID of network.'), constraints=[ constraints.CustomConstraint('neutron.network') ], update_allowed=True ), ANTI_AFFINITY: properties.Schema( properties.Schema.LIST, _("List of processes to enable anti-affinity for."), schema=properties.Schema( properties.Schema.STRING, ), update_allowed=True ), CLUSTER_CONFIGS: properties.Schema( properties.Schema.MAP, _('Cluster configs dictionary.'), update_allowed=True ), NODE_GROUPS: properties.Schema( properties.Schema.LIST, _('Node groups.'), schema=properties.Schema( properties.Schema.MAP, schema={ NG_NAME: properties.Schema( properties.Schema.STRING, _('Name of the Node group.'), required=True ), COUNT: properties.Schema( properties.Schema.INTEGER, _("Number of instances in the Node group."), required=True, constraints=[ constraints.Range(min=1) ] ), NG_TEMPLATE_ID: properties.Schema( properties.Schema.STRING, _("ID of the Node Group Template."), required=True ), } ), update_allowed=True ), USE_AUTOCONFIG: properties.Schema( properties.Schema.BOOLEAN, _("Configure most important configs automatically."), support_status=support.SupportStatus(version='5.0.0') ), SHARES: properties.Schema( properties.Schema.LIST, _("List of manila shares to be mounted."), schema=properties.Schema( properties.Schema.MAP, schema={ SHARE_ID: properties.Schema( properties.Schema.STRING, _("Id of the manila share."), required=True ), PATH: properties.Schema( properties.Schema.STRING, _("Local path on each cluster node on which to mount " "the share. Defaults to '/mnt/{share_id}'.") ), ACCESS_LEVEL: properties.Schema( properties.Schema.STRING, _("Governs permissions set in manila for the cluster " "ips."), constraints=[ constraints.AllowedValues(['rw', 'ro']), ], default='rw' ) } ), support_status=support.SupportStatus(version='6.0.0'), update_allowed=True ) } default_client_name = 'sahara' physical_resource_name_limit = 50 entity = 'cluster_templates' def _cluster_template_name(self): name = self.properties[self.NAME] if name: return name return re.sub('[^a-zA-Z0-9-]', '', self.physical_resource_name()) def _prepare_properties(self): props = { 'name': self._cluster_template_name(), 'plugin_name': self.properties[self.PLUGIN_NAME], 'hadoop_version': self.properties[self.HADOOP_VERSION], 'description': self.properties[self.DESCRIPTION], 'cluster_configs': self.properties[self.CLUSTER_CONFIGS], 'node_groups': self.properties[self.NODE_GROUPS], 'anti_affinity': self.properties[self.ANTI_AFFINITY], 'net_id': self.properties[self.MANAGEMENT_NETWORK], 'default_image_id': self.properties[self.IMAGE_ID], 'use_autoconfig': self.properties[self.USE_AUTOCONFIG], 'shares': self.properties[self.SHARES] } if props['net_id']: if self.is_using_neutron(): props['net_id'] = self.client_plugin( 'neutron').find_neutron_resource( self.properties, self.MANAGEMENT_NETWORK, 'network') else: props['net_id'] = self.client_plugin( 'nova').get_nova_network_id(props['net_id']) return props def handle_create(self): args = self._prepare_properties() cluster_template = self.client().cluster_templates.create(**args) LOG.info(_LI("Cluster Template '%s' has been created"), cluster_template.name) self.resource_id_set(cluster_template.id) return self.resource_id def handle_update(self, json_snippet, tmpl_diff, prop_diff): if prop_diff: self.properties = json_snippet.properties( self.properties_schema, self.context) args = self._prepare_properties() self.client().cluster_templates.update(self.resource_id, **args) def validate(self): res = super(SaharaClusterTemplate, self).validate() if res: return res # check if running on neutron and MANAGEMENT_NETWORK missing if (self.is_using_neutron() and not self.properties[self.MANAGEMENT_NETWORK]): msg = _("%s must be provided" ) % self.MANAGEMENT_NETWORK raise exception.StackValidationFailed(message=msg) self.client_plugin().validate_hadoop_version( self.properties[self.PLUGIN_NAME], self.properties[self.HADOOP_VERSION] )
class CloudDns(resource.Resource): PROPERTIES = ( NAME, EMAIL_ADDRESS, TTL, COMMENT, RECORDS, ) = ( 'name', 'emailAddress', 'ttl', 'comment', 'records', ) _RECORD_KEYS = ( RECORD_COMMENT, RECORD_NAME, RECORD_DATA, RECORD_PRIORITY, RECORD_TTL, RECORD_TYPE, ) = ( 'comment', 'name', 'data', 'priority', 'ttl', 'type', ) properties_schema = { NAME: properties.Schema( properties.Schema.STRING, _('Specifies the name for the domain or subdomain. Must be a ' 'valid domain name.'), required=True, constraints=[ constraints.Length(min=3), ]), EMAIL_ADDRESS: properties.Schema( properties.Schema.STRING, _('Email address to use for contacting the domain administrator.'), required=True, update_allowed=True), TTL: properties.Schema(properties.Schema.INTEGER, _('How long other servers should cache recorddata.'), default=3600, constraints=[ constraints.Range(min=301), ], update_allowed=True), COMMENT: properties.Schema(properties.Schema.STRING, _('Optional free form text comment'), constraints=[ constraints.Length(max=160), ], update_allowed=True), RECORDS: properties.Schema( properties.Schema.LIST, _('Domain records'), schema=properties.Schema( properties.Schema.MAP, schema={ RECORD_COMMENT: properties.Schema(properties.Schema.STRING, _('Optional free form text comment'), constraints=[ constraints.Length(max=160), ]), RECORD_NAME: properties.Schema( properties.Schema.STRING, _('Specifies the name for the domain or ' 'subdomain. Must be a valid domain name.'), required=True, constraints=[ constraints.Length(min=3), ]), RECORD_DATA: properties.Schema(properties.Schema.STRING, _('Type specific record data'), required=True), RECORD_PRIORITY: properties.Schema( properties.Schema.INTEGER, _('Required for MX and SRV records, but ' 'forbidden for other record types. If ' 'specified, must be an integer from 0 to ' '65535.'), constraints=[ constraints.Range(0, 65535), ]), RECORD_TTL: properties.Schema(properties.Schema.INTEGER, _('How long other servers should cache ' 'recorddata.'), default=3600, constraints=[ constraints.Range(min=301), ]), RECORD_TYPE: properties.Schema(properties.Schema.STRING, _('Specifies the record type.'), required=True, constraints=[ constraints.AllowedValues([ 'A', 'AAAA', 'NS', 'MX', 'CNAME', 'TXT', 'SRV' ]), ]), }, ), update_allowed=True), } update_allowed_keys = ('Properties', ) def cloud_dns(self): return self.stack.clients.cloud_dns() def handle_create(self): """ Create a Rackspace CloudDns Instance. """ # There is no check_create_complete as the pyrax create for DNS is # synchronous. logger.debug(_("CloudDns handle_create called.")) args = dict((k, v) for k, v in self.properties.items()) for rec in args[self.RECORDS] or {}: # only pop the priority for the correct types rec_type = rec[self.RECORD_TYPE] if (rec_type != 'MX') and (rec_type != 'SRV'): rec.pop(self.RECORD_PRIORITY, None) dom = self.cloud_dns().create(**args) self.resource_id_set(dom.id) def handle_update(self, json_snippet, tmpl_diff, prop_diff): """ Update a Rackspace CloudDns Instance. """ logger.debug(_("CloudDns handle_update called.")) if not self.resource_id: raise exception.Error(_('Update called on a non-existent domain')) if prop_diff: dom = self.cloud_dns().get(self.resource_id) # handle records separately records = prop_diff.pop(self.RECORDS, {}) # Handle top level domain properties dom.update(**prop_diff) # handle records if records: recs = dom.list_records() # 1. delete all the current records other than rackspace NS records [ rec.delete() for rec in recs if rec.type != 'NS' or 'stabletransit.com' not in rec.data ] # 2. update with the new records in prop_diff dom.add_records(records) def handle_delete(self): """ Delete a Rackspace CloudDns Instance. """ logger.debug(_("CloudDns handle_delete called.")) if self.resource_id: try: dom = self.cloud_dns().get(self.resource_id) dom.delete() except NotFound: pass self.resource_id_set(None)
class SaharaNodeGroupTemplate(resource.Resource): support_status = support.SupportStatus(version='2014.2') PROPERTIES = ( NAME, PLUGIN_NAME, HADOOP_VERSION, FLAVOR, DESCRIPTION, VOLUMES_PER_NODE, VOLUMES_SIZE, VOLUME_TYPE, SECURITY_GROUPS, AUTO_SECURITY_GROUP, AVAILABILITY_ZONE, VOLUMES_AVAILABILITY_ZONE, NODE_PROCESSES, FLOATING_IP_POOL, NODE_CONFIGS, IMAGE_ID, IS_PROXY_GATEWAY, VOLUME_LOCAL_TO_INSTANCE, USE_AUTOCONFIG, SHARES ) = ( 'name', 'plugin_name', 'hadoop_version', 'flavor', 'description', 'volumes_per_node', 'volumes_size', 'volume_type', 'security_groups', 'auto_security_group', 'availability_zone', 'volumes_availability_zone', 'node_processes', 'floating_ip_pool', 'node_configs', 'image_id', 'is_proxy_gateway', 'volume_local_to_instance', 'use_autoconfig', 'shares' ) _SHARE_KEYS = ( SHARE_ID, PATH, ACCESS_LEVEL ) = ( 'id', 'path', 'access_level' ) properties_schema = { NAME: properties.Schema( properties.Schema.STRING, _("Name for the Sahara Node Group Template."), constraints=[ constraints.Length(min=1, max=50), constraints.AllowedPattern(SAHARA_NAME_REGEX), ], update_allowed=True ), DESCRIPTION: properties.Schema( properties.Schema.STRING, _('Description of the Node Group Template.'), default="", update_allowed=True ), PLUGIN_NAME: properties.Schema( properties.Schema.STRING, _('Plugin name.'), required=True, constraints=[ constraints.CustomConstraint('sahara.plugin') ], update_allowed=True ), HADOOP_VERSION: properties.Schema( properties.Schema.STRING, _('Version of Hadoop running on instances.'), required=True, update_allowed=True ), FLAVOR: properties.Schema( properties.Schema.STRING, _('Name or ID Nova flavor for the nodes.'), required=True, constraints=[ constraints.CustomConstraint('nova.flavor') ], update_allowed=True ), VOLUMES_PER_NODE: properties.Schema( properties.Schema.INTEGER, _("Volumes per node."), constraints=[ constraints.Range(min=0), ], update_allowed=True ), VOLUMES_SIZE: properties.Schema( properties.Schema.INTEGER, _("Size of the volumes, in GB."), constraints=[ constraints.Range(min=1), ], update_allowed=True ), VOLUME_TYPE: properties.Schema( properties.Schema.STRING, _("Type of the volume to create on Cinder backend."), constraints=[ constraints.CustomConstraint('cinder.vtype') ], update_allowed=True ), SECURITY_GROUPS: properties.Schema( properties.Schema.LIST, _("List of security group names or IDs to assign to this " "Node Group template."), schema=properties.Schema( properties.Schema.STRING, ), update_allowed=True ), AUTO_SECURITY_GROUP: properties.Schema( properties.Schema.BOOLEAN, _("Defines whether auto-assign security group to this " "Node Group template."), update_allowed=True ), AVAILABILITY_ZONE: properties.Schema( properties.Schema.STRING, _("Availability zone to create servers in."), update_allowed=True ), VOLUMES_AVAILABILITY_ZONE: properties.Schema( properties.Schema.STRING, _("Availability zone to create volumes in."), update_allowed=True ), NODE_PROCESSES: properties.Schema( properties.Schema.LIST, _("List of processes to run on every node."), required=True, constraints=[ constraints.Length(min=1), ], schema=properties.Schema( properties.Schema.STRING, ), update_allowed=True ), FLOATING_IP_POOL: properties.Schema( properties.Schema.STRING, _("Name or UUID of the Neutron floating IP network or " "name of the Nova floating ip pool to use. " "Should not be provided when used with Nova-network " "that auto-assign floating IPs."), update_allowed=True ), NODE_CONFIGS: properties.Schema( properties.Schema.MAP, _("Dictionary of node configurations."), update_allowed=True ), IMAGE_ID: properties.Schema( properties.Schema.STRING, _("ID of the image to use for the template."), constraints=[ constraints.CustomConstraint('sahara.image'), ], update_allowed=True ), IS_PROXY_GATEWAY: properties.Schema( properties.Schema.BOOLEAN, _("Provide access to nodes using other nodes of the cluster " "as proxy gateways."), support_status=support.SupportStatus(version='5.0.0'), update_allowed=True ), VOLUME_LOCAL_TO_INSTANCE: properties.Schema( properties.Schema.BOOLEAN, _("Create volumes on the same physical port as an instance."), support_status=support.SupportStatus(version='5.0.0'), update_allowed=True ), USE_AUTOCONFIG: properties.Schema( properties.Schema.BOOLEAN, _("Configure most important configs automatically."), support_status=support.SupportStatus(version='5.0.0'), update_allowed=True ), SHARES: properties.Schema( properties.Schema.LIST, _("List of manila shares to be mounted."), schema=properties.Schema( properties.Schema.MAP, schema={ SHARE_ID: properties.Schema( properties.Schema.STRING, _("Id of the manila share."), required=True ), PATH: properties.Schema( properties.Schema.STRING, _("Local path on each cluster node on which to mount " "the share. Defaults to '/mnt/{share_id}'.") ), ACCESS_LEVEL: properties.Schema( properties.Schema.STRING, _("Governs permissions set in manila for the cluster " "ips."), constraints=[ constraints.AllowedValues(['rw', 'ro']), ], default='rw' ) } ), support_status=support.SupportStatus(version='6.0.0'), update_allowed=True ) } default_client_name = 'sahara' physical_resource_name_limit = 50 entity = 'node_group_templates' def _ngt_name(self): name = self.properties[self.NAME] if name: return name return re.sub('[^a-zA-Z0-9-]', '', self.physical_resource_name()) def _prepare_properties(self): props = { 'name': self._ngt_name(), 'plugin_name': self.properties[self.PLUGIN_NAME], 'hadoop_version': self.properties[self.HADOOP_VERSION], 'flavor_id': self.client_plugin("nova").find_flavor_by_name_or_id( self.properties[self.FLAVOR]), 'description': self.properties[self.DESCRIPTION], 'volumes_per_node': self.properties[self.VOLUMES_PER_NODE], 'volumes_size': self.properties[self.VOLUMES_SIZE], 'node_processes': self.properties[self.NODE_PROCESSES], 'node_configs': self.properties[self.NODE_CONFIGS], 'floating_ip_pool': self.properties[self.FLOATING_IP_POOL], 'security_groups': self.properties[self.SECURITY_GROUPS], 'auto_security_group': self.properties[self.AUTO_SECURITY_GROUP], 'availability_zone': self.properties[self.AVAILABILITY_ZONE], 'volumes_availability_zone': self.properties[ self.VOLUMES_AVAILABILITY_ZONE], 'volume_type': self.properties[self.VOLUME_TYPE], 'image_id': self.properties[self.IMAGE_ID], 'is_proxy_gateway': self.properties[self.IS_PROXY_GATEWAY], 'volume_local_to_instance': self.properties[ self.VOLUME_LOCAL_TO_INSTANCE], 'use_autoconfig': self.properties[self.USE_AUTOCONFIG], 'shares': self.properties[self.SHARES] } if props['floating_ip_pool'] and self.is_using_neutron(): props['floating_ip_pool'] = self.client_plugin( 'neutron').find_neutron_resource( self.properties, self.FLOATING_IP_POOL, 'network') return props def handle_create(self): args = self._prepare_properties() node_group_template = self.client().node_group_templates.create(**args) LOG.info(_LI("Node Group Template '%s' has been created"), node_group_template.name) self.resource_id_set(node_group_template.id) return self.resource_id def handle_update(self, json_snippet, tmpl_diff, prop_diff): if prop_diff: self.properties = json_snippet.properties( self.properties_schema, self.context) args = self._prepare_properties() self.client().node_group_templates.update(self.resource_id, **args) def validate(self): res = super(SaharaNodeGroupTemplate, self).validate() if res: return res pool = self.properties[self.FLOATING_IP_POOL] if pool: if self.is_using_neutron(): try: self.client_plugin('neutron').find_neutron_resource( self.properties, self.FLOATING_IP_POOL, 'network') except Exception as ex: if (self.client_plugin('neutron').is_not_found(ex) or self.client_plugin('neutron').is_no_unique(ex)): raise exception.StackValidationFailed( message=ex.message) raise else: try: self.client('nova').floating_ip_pools.find(name=pool) except Exception as ex: if self.client_plugin('nova').is_not_found(ex): raise exception.StackValidationFailed( message=ex.message) raise self.client_plugin().validate_hadoop_version( self.properties[self.PLUGIN_NAME], self.properties[self.HADOOP_VERSION] ) # validate node processes plugin = self.client().plugins.get_version_details( self.properties[self.PLUGIN_NAME], self.properties[self.HADOOP_VERSION]) allowed_processes = [item for sublist in list(six.itervalues(plugin.node_processes)) for item in sublist] unsupported_processes = [] for process in self.properties[self.NODE_PROCESSES]: if process not in allowed_processes: unsupported_processes.append(process) if unsupported_processes: msg = (_("Plugin %(plugin)s doesn't support the following " "node processes: %(unsupported)s. Allowed processes are: " "%(allowed)s") % {'plugin': self.properties[self.PLUGIN_NAME], 'unsupported': ', '.join(unsupported_processes), 'allowed': ', '.join(allowed_processes)}) raise exception.StackValidationFailed( path=[self.stack.t.get_section_name('resources'), self.name, self.stack.t.get_section_name('properties')], message=msg)
class DesignateRecordSet(resource.Resource): """Heat Template Resource for Designate RecordSet. Designate provides DNS-as-a-Service services for OpenStack. RecordSet helps to add more than one records. """ support_status = support.SupportStatus(version='8.0.0') PROPERTIES = (NAME, TTL, DESCRIPTION, TYPE, RECORDS, ZONE) = ('name', 'ttl', 'description', 'type', 'records', 'zone') _ALLOWED_TYPES = (A, AAAA, CNAME, MX, SRV, TXT, SPF, NS, PTR, SSHFP, SOA) = ('A', 'AAAA', 'CNAME', 'MX', 'SRV', 'TXT', 'SPF', 'NS', 'PTR', 'SSHFP', 'SOA') properties_schema = { # Based on RFC 1035, length of name is set to max of 255 NAME: properties.Schema(properties.Schema.STRING, _('RecordSet name.'), constraints=[constraints.Length(max=255)]), # Based on RFC 1035, range for ttl is set to 1 to signed 32 bit number TTL: properties.Schema( properties.Schema.INTEGER, _('Time To Live (Seconds).'), update_allowed=True, constraints=[constraints.Range(min=1, max=2147483647)]), # designate mandates to the max length of 160 for description DESCRIPTION: properties.Schema(properties.Schema.STRING, _('Description of RecordSet.'), update_allowed=True, constraints=[constraints.Length(max=160)]), TYPE: properties.Schema( properties.Schema.STRING, _('DNS RecordSet type.'), required=True, constraints=[constraints.AllowedValues(_ALLOWED_TYPES)]), RECORDS: properties.Schema( properties.Schema.LIST, _('A list of data for this RecordSet. Each item will be a ' 'separate record in Designate These items should conform to the ' 'DNS spec for the record type - e.g. A records must be IPv4 ' 'addresses, CNAME records must be a hostname. DNS record data ' 'varies based on the type of record. For more details, please ' 'refer rfc 1035.'), update_allowed=True, required=True), ZONE: properties.Schema( properties.Schema.STRING, _('DNS Zone id or name.'), required=True, constraints=[constraints.CustomConstraint('designate.zone')]), } default_client_name = 'designate' entity = 'recordsets' def handle_create(self): args = dict((k, v) for k, v in six.iteritems(self.properties) if v) args['type_'] = args.pop(self.TYPE) if not args.get(self.NAME): args[self.NAME] = self.physical_resource_name() rs = self.client().recordsets.create(**args) self.resource_id_set(rs['id']) def _check_status_complete(self): recordset = self.client().recordsets.get( recordset=self.resource_id, zone=self.properties[self.ZONE]) if recordset['status'] == 'ERROR': raise exception.ResourceInError( resource_status=recordset['status'], status_reason=_('Error in RecordSet')) return recordset['status'] != 'PENDING' def check_create_complete(self, handler_data=None): return self._check_status_complete() def handle_update(self, json_snippet, tmpl_diff, prop_diff): args = dict() for prp in (self.TTL, self.DESCRIPTION, self.RECORDS): if prop_diff.get(prp): args[prp] = prop_diff.get(prp) if prop_diff.get(self.TYPE): args['type_'] = prop_diff.get(self.TYPE) if len(args.keys()) > 0: self.client().recordsets.update(recordset=self.resource_id, zone=self.properties[self.ZONE], values=args) def check_update_complete(self, handler_data=None): return self._check_status_complete() def handle_delete(self): if self.resource_id is not None: with self.client_plugin().ignore_not_found: self.client().recordsets.delete( recordset=self.resource_id, zone=self.properties[self.ZONE]) return self.resource_id def check_delete_complete(self, handler_data=None): if handler_data: with self.client_plugin().ignore_not_found: return self._check_status_complete() return True def _show_resource(self): return self.client().recordsets.get(recordset=self.resource_id, zone=self.properties[self.ZONE])
def test_length_invalid_type(self): schema = constraints.Schema('Integer', constraints=[constraints.Length(1, 10)]) err = self.assertRaises(exception.InvalidSchemaError, schema.validate) self.assertIn('Length constraint invalid for Integer', six.text_type(err))
class NetworkGateway(neutron.NeutronResource): ''' A resource for the Network Gateway resource in Neutron Network Gateway. ''' PROPERTIES = ( NAME, DEVICES, CONNECTIONS, ) = ( 'name', 'devices', 'connections', ) _DEVICES_KEYS = ( ID, INTERFACE_NAME, ) = ( 'id', 'interface_name', ) _CONNECTIONS_KEYS = ( NETWORK_ID, SEGMENTATION_TYPE, SEGMENTATION_ID, ) = ( 'network_id', 'segmentation_type', 'segmentation_id', ) properties_schema = { NAME: properties.Schema(properties.Schema.STRING, description=_('The name of the network gateway.'), update_allowed=True), DEVICES: properties.Schema( properties.Schema.LIST, description=_('Device info for this network gateway.'), required=True, constraints=[constraints.Length(min=1)], update_allowed=True, schema=properties.Schema( properties.Schema.MAP, schema={ ID: properties.Schema(properties.Schema.STRING, description=_( 'The device id for the network ' 'gateway.'), required=True), INTERFACE_NAME: properties.Schema(properties.Schema.STRING, description=_( 'The interface name for the ' 'network gateway.'), required=True) })), CONNECTIONS: properties.Schema( properties.Schema.LIST, description=_('Connection info for this network gateway.'), default={}, update_allowed=True, schema=properties.Schema( properties.Schema.MAP, schema={ NETWORK_ID: properties.Schema( properties.Schema.STRING, description=_( 'The id of internal network to connect on ' 'the network gateway.'), required=True), SEGMENTATION_TYPE: properties.Schema( properties.Schema.STRING, description=_( 'L2 segmentation strategy on the external ' 'side of the network gateway.'), default='flat', constraints=[ constraints.AllowedValues(('flat', 'vlan')) ]), SEGMENTATION_ID: properties.Schema( properties.Schema.INTEGER, description=_( 'The id for L2 segment on the external side ' 'of the network gateway. Must be specified ' 'when using vlan.'), constraints=[constraints.Range(0, 4094)]) })) } attributes_schema = { "default": _("A boolean value of default flag."), "show": _("All attributes.") } update_allowed_keys = ('Properties', ) def _show_resource(self): return self.neutron().show_network_gateway( self.resource_id)['network_gateway'] def validate(self): ''' Validate any of the provided params ''' super(NetworkGateway, self).validate() connections = self.properties[self.CONNECTIONS] for connection in connections: segmentation_type = connection[self.SEGMENTATION_TYPE] segmentation_id = connection.get(self.SEGMENTATION_ID) if segmentation_type == 'vlan' and segmentation_id is None: msg = _("segmentation_id must be specified for using vlan") raise exception.StackValidationFailed(message=msg) if segmentation_type == 'flat' and segmentation_id: msg = _("segmentation_id cannot be specified except 0 for " "using flat") raise exception.StackValidationFailed(message=msg) def handle_create(self): props = self.prepare_properties(self.properties, self.physical_resource_name()) connections = props.pop(self.CONNECTIONS) ret = self.neutron().create_network_gateway({'network_gateway': props})['network_gateway'] for connection in connections: self.neutron().connect_network_gateway(ret['id'], connection) self.resource_id_set(ret['id']) def handle_delete(self): if not self.resource_id: return client = self.neutron() connections = self.properties[self.CONNECTIONS] for connection in connections: try: client.disconnect_network_gateway(self.resource_id, connection) except NeutronClientException as ex: self._handle_not_found_exception(ex) try: client.delete_network_gateway(self.resource_id) except NeutronClientException as ex: self._handle_not_found_exception(ex) else: return self._delete_task() def handle_update(self, json_snippet, tmpl_diff, prop_diff): props = self.prepare_update_properties(json_snippet) connections = props.pop(self.CONNECTIONS) if self.DEVICES in prop_diff: self.handle_delete() self.properties.data.update(props) self.handle_create() return else: props.pop(self.DEVICES, None) if self.NAME in prop_diff: self.neutron().update_network_gateway(self.resource_id, {'network_gateway': props}) if self.CONNECTIONS in prop_diff: for connection in self.properties[self.CONNECTIONS]: try: self.neutron().disconnect_network_gateway( self.resource_id, connection) except NeutronClientException as ex: self._handle_not_found_exception(ex) for connection in connections: self.neutron().connect_network_gateway(self.resource_id, connection)
def test_length_min_schema(self): d = {'length': {'min': 5}, 'description': 'a length range'} r = constraints.Length(min=5, description='a length range') self.assertEqual(d, dict(r))
class DesignateDomain(resource.Resource): """Heat Template Resource for Designate Domain.""" support_status = support.SupportStatus(version='5.0.0') PROPERTIES = (NAME, TTL, DESCRIPTION, EMAIL) = ('name', 'ttl', 'description', 'email') ATTRIBUTES = (SERIAL, ) = ('serial', ) properties_schema = { # Based on RFC 1035, length of name is set to max of 255 NAME: properties.Schema(properties.Schema.STRING, _('Domain name.'), required=True, constraints=[constraints.Length(max=255)]), # Based on RFC 1035, range for ttl is set to 0 to signed 32 bit number TTL: properties.Schema( properties.Schema.INTEGER, _('Time To Live (Seconds).'), update_allowed=True, constraints=[constraints.Range(min=0, max=2147483647)]), # designate mandates to the max length of 160 for description DESCRIPTION: properties.Schema(properties.Schema.STRING, _('Description of domain.'), update_allowed=True, constraints=[constraints.Length(max=160)]), EMAIL: properties.Schema(properties.Schema.STRING, _('Domain email.'), update_allowed=True, required=True) } attributes_schema = { SERIAL: attributes.Schema(_("DNS domain serial."), type=attributes.Schema.STRING), } default_client_name = 'designate' def handle_create(self): args = dict(name=self.properties[self.NAME], email=self.properties[self.EMAIL], description=self.properties[self.DESCRIPTION], ttl=self.properties[self.TTL]) domain = self.client_plugin().domain_create(**args) self.resource_id_set(domain.id) def handle_update(self, json_snippet=None, tmpl_diff=None, prop_diff=None): args = dict() if prop_diff.get(self.EMAIL): args['email'] = prop_diff.get(self.EMAIL) if prop_diff.get(self.TTL): args['ttl'] = prop_diff.get(self.TTL) if prop_diff.get(self.DESCRIPTION): args['description'] = prop_diff.get(self.DESCRIPTION) if len(args.keys()) > 0: args['id'] = self.resource_id self.client_plugin().domain_update(**args) def handle_delete(self): if self.resource_id is not None: try: self.client().domains.delete(self.resource_id) except Exception as ex: self.client_plugin().ignore_not_found(ex) def _resolve_attribute(self, name): if name == self.SERIAL: domain = self.client().domains.get(self.resource_id) return domain.serial
def test_length_validate(self): l = constraints.Length(min=5, max=5, description='a range') l.validate('abcde')
class NetworkGateway(neutron.NeutronResource): """Network Gateway resource in Neutron Network Gateway. Resource for connecting internal networks with specified devices. """ support_status = support.SupportStatus(version='2014.1') PROPERTIES = ( NAME, DEVICES, CONNECTIONS, ) = ( 'name', 'devices', 'connections', ) ATTRIBUTES = (DEFAULT, ) = ('default', ) _DEVICES_KEYS = ( ID, INTERFACE_NAME, ) = ( 'id', 'interface_name', ) _CONNECTIONS_KEYS = ( NETWORK_ID, NETWORK, SEGMENTATION_TYPE, SEGMENTATION_ID, ) = ( 'network_id', 'network', 'segmentation_type', 'segmentation_id', ) properties_schema = { NAME: properties.Schema(properties.Schema.STRING, description=_('The name of the network gateway.'), update_allowed=True), DEVICES: properties.Schema( properties.Schema.LIST, description=_('Device info for this network gateway.'), required=True, constraints=[constraints.Length(min=1)], update_allowed=True, schema=properties.Schema( properties.Schema.MAP, schema={ ID: properties.Schema(properties.Schema.STRING, description=_( 'The device id for the network ' 'gateway.'), required=True), INTERFACE_NAME: properties.Schema(properties.Schema.STRING, description=_( 'The interface name for the ' 'network gateway.'), required=True) })), CONNECTIONS: properties.Schema( properties.Schema.LIST, description=_('Connection info for this network gateway.'), default={}, update_allowed=True, schema=properties.Schema( properties.Schema.MAP, schema={ NETWORK_ID: properties.Schema( properties.Schema.STRING, support_status=support.SupportStatus( status=support.HIDDEN, message=_('Use property %s.') % NETWORK, version='5.0.0', previous_status=support.SupportStatus( status=support.DEPRECATED, version='2014.2')), constraints=[ constraints.CustomConstraint('neutron.network') ], ), NETWORK: properties.Schema( properties.Schema.STRING, description=_('The internal network to connect on ' 'the network gateway.'), support_status=support.SupportStatus(version='2014.2'), required=True, constraints=[ constraints.CustomConstraint('neutron.network') ], ), SEGMENTATION_TYPE: properties.Schema( properties.Schema.STRING, description=_( 'L2 segmentation strategy on the external ' 'side of the network gateway.'), default='flat', constraints=[ constraints.AllowedValues(('flat', 'vlan')) ]), SEGMENTATION_ID: properties.Schema( properties.Schema.INTEGER, description=_( 'The id for L2 segment on the external side ' 'of the network gateway. Must be specified ' 'when using vlan.'), constraints=[constraints.Range(0, 4094)]) })) } attributes_schema = { DEFAULT: attributes.Schema(_("A boolean value of default flag."), type=attributes.Schema.STRING), } def translation_rules(self, props): return [ translation.TranslationRule(props, translation.TranslationRule.REPLACE, [self.CONNECTIONS, self.NETWORK], value_name=self.NETWORK_ID) ] def _show_resource(self): return self.client().show_network_gateway( self.resource_id)['network_gateway'] def validate(self): """Validate any of the provided params.""" super(NetworkGateway, self).validate() connections = self.properties[self.CONNECTIONS] for connection in connections: segmentation_type = connection[self.SEGMENTATION_TYPE] segmentation_id = connection.get(self.SEGMENTATION_ID) if segmentation_type == 'vlan' and segmentation_id is None: msg = _("segmentation_id must be specified for using vlan") raise exception.StackValidationFailed(message=msg) if segmentation_type == 'flat' and segmentation_id: msg = _("segmentation_id cannot be specified except 0 for " "using flat") raise exception.StackValidationFailed(message=msg) def handle_create(self): props = self.prepare_properties(self.properties, self.physical_resource_name()) connections = props.pop(self.CONNECTIONS) ret = self.client().create_network_gateway({'network_gateway': props})['network_gateway'] self.resource_id_set(ret['id']) for connection in connections: self.client_plugin().resolve_network(connection, self.NETWORK, 'network_id') if self.NETWORK in six.iterkeys(connection): connection.pop(self.NETWORK) self.client().connect_network_gateway(ret['id'], connection) def handle_delete(self): if not self.resource_id: return connections = self.properties[self.CONNECTIONS] for connection in connections: with self.client_plugin().ignore_not_found: self.client_plugin().resolve_network(connection, self.NETWORK, 'network_id') if self.NETWORK in six.iterkeys(connection): connection.pop(self.NETWORK) self.client().disconnect_network_gateway( self.resource_id, connection) try: self.client().delete_network_gateway(self.resource_id) except Exception as ex: self.client_plugin().ignore_not_found(ex) else: return True def handle_update(self, json_snippet, tmpl_diff, prop_diff): connections = None if self.CONNECTIONS in prop_diff: connections = prop_diff.pop(self.CONNECTIONS) if self.DEVICES in prop_diff: self.handle_delete() self.properties.data.update(prop_diff) self.handle_create() return if prop_diff: self.prepare_update_properties(prop_diff) self.client().update_network_gateway( self.resource_id, {'network_gateway': prop_diff}) if connections: for connection in self.properties[self.CONNECTIONS]: with self.client_plugin().ignore_not_found: self.client_plugin().resolve_network( connection, self.NETWORK, 'network_id') if self.NETWORK in six.iterkeys(connection): connection.pop(self.NETWORK) self.client().disconnect_network_gateway( self.resource_id, connection) for connection in connections: self.client_plugin().resolve_network(connection, self.NETWORK, 'network_id') if self.NETWORK in six.iterkeys(connection): connection.pop(self.NETWORK) self.client().connect_network_gateway(self.resource_id, connection)
class OSDBInstance(resource.Resource): ''' OpenStack cloud database instance resource. ''' support_status = support.SupportStatus(version='2014.1') TROVE_STATUS = ( ERROR, FAILED, ACTIVE, ) = ( 'ERROR', 'FAILED', 'ACTIVE', ) TROVE_STATUS_REASON = { FAILED: _('The database instance was created, but heat failed to set ' 'up the datastore. If a database instance is in the FAILED ' 'state, it should be deleted and a new one should be ' 'created.'), ERROR: _('The last operation for the database instance failed due to ' 'an error.'), } BAD_STATUSES = (ERROR, FAILED) PROPERTIES = ( NAME, FLAVOR, SIZE, DATABASES, USERS, AVAILABILITY_ZONE, RESTORE_POINT, DATASTORE_TYPE, DATASTORE_VERSION, NICS, ) = ( 'name', 'flavor', 'size', 'databases', 'users', 'availability_zone', 'restore_point', 'datastore_type', 'datastore_version', 'networks', ) _DATABASE_KEYS = ( DATABASE_CHARACTER_SET, DATABASE_COLLATE, DATABASE_NAME, ) = ( 'character_set', 'collate', 'name', ) _USER_KEYS = ( USER_NAME, USER_PASSWORD, USER_HOST, USER_DATABASES, ) = ( 'name', 'password', 'host', 'databases', ) _NICS_KEYS = (NET, PORT, V4_FIXED_IP) = ('network', 'port', 'fixed_ip') ATTRIBUTES = ( HOSTNAME, HREF, ) = ( 'hostname', 'href', ) properties_schema = { NAME: properties.Schema(properties.Schema.STRING, _('Name of the DB instance to create.'), constraints=[ constraints.Length(max=255), ]), FLAVOR: properties.Schema( properties.Schema.STRING, _('Reference to a flavor for creating DB instance.'), required=True, constraints=[constraints.CustomConstraint('trove.flavor')]), DATASTORE_TYPE: properties.Schema(properties.Schema.STRING, _("Name of registered datastore type."), constraints=[constraints.Length(max=255)]), DATASTORE_VERSION: properties.Schema( properties.Schema.STRING, _("Name of the registered datastore version. " "It must exist for provided datastore type. " "Defaults to using single active version. " "If several active versions exist for provided datastore type, " "explicit value for this parameter must be specified."), constraints=[constraints.Length(max=255)]), SIZE: properties.Schema(properties.Schema.INTEGER, _('Database volume size in GB.'), required=True, constraints=[ constraints.Range(1, 150), ]), NICS: properties.Schema( properties.Schema.LIST, _("List of network interfaces to create on instance."), default=[], schema=properties.Schema( properties.Schema.MAP, schema={ NET: properties.Schema( properties.Schema.STRING, _('Name or UUID of the network to attach this NIC to. ' 'Either %(port)s or %(net)s must be specified.') % { 'port': PORT, 'net': NET }, constraints=[ constraints.CustomConstraint('neutron.network') ]), PORT: properties.Schema( properties.Schema.STRING, _('Name or UUID of Neutron port to attach this ' 'NIC to. ' 'Either %(port)s or %(net)s must be specified.') % { 'port': PORT, 'net': NET }, constraints=[ constraints.CustomConstraint('neutron.port') ], ), V4_FIXED_IP: properties.Schema(properties.Schema.STRING, _('Fixed IPv4 address for this NIC.')), }, ), ), DATABASES: properties.Schema( properties.Schema.LIST, _('List of databases to be created on DB instance creation.'), default=[], schema=properties.Schema( properties.Schema.MAP, schema={ DATABASE_CHARACTER_SET: properties.Schema(properties.Schema.STRING, _('Set of symbols and encodings.'), default='utf8'), DATABASE_COLLATE: properties.Schema( properties.Schema.STRING, _('Set of rules for comparing characters in a ' 'character set.'), default='utf8_general_ci'), DATABASE_NAME: properties.Schema( properties.Schema.STRING, _('Specifies database names for creating ' 'databases on instance creation.'), required=True, constraints=[ constraints.Length(max=64), constraints.AllowedPattern(r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ]), }, )), USERS: properties.Schema( properties.Schema.LIST, _('List of users to be created on DB instance creation.'), default=[], schema=properties.Schema( properties.Schema.MAP, schema={ USER_NAME: properties.Schema( properties.Schema.STRING, _('User name to create a user on instance ' 'creation.'), required=True, constraints=[ constraints.Length(max=16), constraints.AllowedPattern(r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ]), USER_PASSWORD: properties.Schema(properties.Schema.STRING, _('Password for those users on instance ' 'creation.'), required=True, constraints=[ constraints.AllowedPattern( r'[a-zA-Z0-9_]+' r'[a-zA-Z0-9_@?#\s]*' r'[a-zA-Z0-9_]+'), ]), USER_HOST: properties.Schema( properties.Schema.STRING, _('The host from which a user is allowed to ' 'connect to the database.'), default='%'), USER_DATABASES: properties.Schema( properties.Schema.LIST, _('Names of databases that those users can ' 'access on instance creation.'), schema=properties.Schema(properties.Schema.STRING, ), required=True, constraints=[ constraints.Length(min=1), ]), }, )), AVAILABILITY_ZONE: properties.Schema(properties.Schema.STRING, _('Name of the availability zone for DB instance.')), RESTORE_POINT: properties.Schema(properties.Schema.STRING, _('DB instance restore point.')), } attributes_schema = { HOSTNAME: attributes.Schema(_("Hostname of the instance.")), HREF: attributes.Schema(_("Api endpoint reference of the instance.")), } default_client_name = 'trove' def __init__(self, name, json_snippet, stack): super(OSDBInstance, self).__init__(name, json_snippet, stack) self._href = None self._dbinstance = None @property def dbinstance(self): """Get the trove dbinstance.""" if not self._dbinstance and self.resource_id: self._dbinstance = self.trove().instances.get(self.resource_id) return self._dbinstance def _dbinstance_name(self): name = self.properties.get(self.NAME) if name: return name return self.physical_resource_name() def handle_create(self): ''' Create cloud database instance. ''' self.flavor = self.client_plugin().get_flavor_id( self.properties[self.FLAVOR]) self.volume = {'size': self.properties[self.SIZE]} self.databases = self.properties.get(self.DATABASES) self.users = self.properties.get(self.USERS) restore_point = self.properties.get(self.RESTORE_POINT) if restore_point: restore_point = {"backupRef": restore_point} zone = self.properties.get(self.AVAILABILITY_ZONE) self.datastore_type = self.properties.get(self.DATASTORE_TYPE) self.datastore_version = self.properties.get(self.DATASTORE_VERSION) # convert user databases to format required for troveclient. # that is, list of database dictionaries for user in self.users: dbs = [{'name': db} for db in user.get(self.USER_DATABASES, [])] user[self.USER_DATABASES] = dbs # convert networks to format required by troveclient nics = [] for nic in self.properties.get(self.NICS): nic_dict = {} net = nic.get(self.NET) if net: if self.is_using_neutron(): net_id = ( self.client_plugin('neutron').find_neutron_resource( nic, self.NET, 'network')) else: net_id = ( self.client_plugin('nova').get_nova_network_id(net)) nic_dict['net-id'] = net_id port = nic.get(self.PORT) if port: neutron = self.client_plugin('neutron') nic_dict['port-id'] = neutron.find_neutron_resource( self.properties, self.PORT, 'port') ip = nic.get(self.V4_FIXED_IP) if ip: nic_dict['v4-fixed-ip'] = ip nics.append(nic_dict) # create db instance instance = self.trove().instances.create( self._dbinstance_name(), self.flavor, volume=self.volume, databases=self.databases, users=self.users, restorePoint=restore_point, availability_zone=zone, datastore=self.datastore_type, datastore_version=self.datastore_version, nics=nics) self.resource_id_set(instance.id) return instance.id def _refresh_instance(self, instance_id): try: instance = self.trove().instances.get(instance_id) return instance except Exception as exc: if self.client_plugin().is_over_limit(exc): LOG.warn( _LW("Stack %(name)s (%(id)s) received an " "OverLimit response during instance.get():" " %(exception)s"), { 'name': self.stack.name, 'id': self.stack.id, 'exception': exc }) return None else: raise def check_create_complete(self, instance_id): ''' Check if cloud DB instance creation is complete. ''' instance = self._refresh_instance(instance_id) # refresh attributes if instance is None: return False if instance.status in self.BAD_STATUSES: raise resource.ResourceInError( resource_status=instance.status, status_reason=self.TROVE_STATUS_REASON.get( instance.status, _("Unknown"))) if instance.status != self.ACTIVE: return False LOG.info( _LI("Database instance %(database)s created (flavor:%(" "flavor)s,volume:%(volume)s, datastore:%(" "datastore_type)s, datastore_version:%(" "datastore_version)s)"), { 'database': self._dbinstance_name(), 'flavor': self.flavor, 'volume': self.volume, 'datastore_type': self.datastore_type, 'datastore_version': self.datastore_version }) return True def handle_check(self): instance = self.trove().instances.get(self.resource_id) status = instance.status checks = [ { 'attr': 'status', 'expected': self.ACTIVE, 'current': status }, ] self._verify_check_conditions(checks) def handle_delete(self): ''' Delete a cloud database instance. ''' if not self.resource_id: return try: instance = self.trove().instances.get(self.resource_id) except Exception as ex: self.client_plugin().ignore_not_found(ex) else: instance.delete() return instance.id def check_delete_complete(self, instance_id): ''' Check for completion of cloud DB instance deletion ''' if not instance_id: return True try: # For some time trove instance may continue to live self._refresh_instance(instance_id) except Exception as ex: self.client_plugin().ignore_not_found(ex) return True return False def validate(self): ''' Validate any of the provided params ''' res = super(OSDBInstance, self).validate() if res: return res datastore_type = self.properties.get(self.DATASTORE_TYPE) datastore_version = self.properties.get(self.DATASTORE_VERSION) self.client_plugin().validate_datastore(datastore_type, datastore_version, self.DATASTORE_TYPE, self.DATASTORE_VERSION) # check validity of user and databases users = self.properties.get(self.USERS) if users: databases = self.properties.get(self.DATABASES) if not databases: msg = _('Databases property is required if users property ' 'is provided for resource %s.') % self.name raise exception.StackValidationFailed(message=msg) db_names = set([db[self.DATABASE_NAME] for db in databases]) for user in users: missing_db = [ db_name for db_name in user[self.USER_DATABASES] if db_name not in db_names ] if missing_db: msg = (_('Database %(dbs)s specified for user does ' 'not exist in databases for resource %(name)s.') % { 'dbs': missing_db, 'name': self.name }) raise exception.StackValidationFailed(message=msg) # check validity of NICS is_neutron = self.is_using_neutron() nics = self.properties.get(self.NICS) for nic in nics: if not is_neutron and nic.get(self.PORT): msg = _("Can not use %s property on Nova-network.") % self.PORT raise exception.StackValidationFailed(message=msg) if bool(nic.get(self.NET)) == bool(nic.get(self.PORT)): msg = _("Either %(net)s or %(port)s must be provided.") % { 'net': self.NET, 'port': self.PORT } raise exception.StackValidationFailed(message=msg) def href(self): if not self._href and self.dbinstance: if not self.dbinstance.links: self._href = None else: for link in self.dbinstance.links: if link['rel'] == 'self': self._href = link[self.HREF] break return self._href def _resolve_attribute(self, name): if name == self.HOSTNAME: return self.dbinstance.hostname elif name == self.HREF: return self.href()
class KeyPair(resource.Resource): """A resource for creating Nova key pairs. A keypair is a ssh key that can be injected into a server on launch. **Note** that if a new key is generated setting `save_private_key` to `True` results in the system saving the private key which can then be retrieved via the `private_key` attribute of this resource. Setting the `public_key` property means that the `private_key` attribute of this resource will always return an empty string regardless of the `save_private_key` setting since there will be no private key data to save. """ support_status = support.SupportStatus(version='2014.1') PROPERTIES = ( NAME, SAVE_PRIVATE_KEY, PUBLIC_KEY, KEY_TYPE, USER, ) = ( 'name', 'save_private_key', 'public_key', 'type', 'user', ) ATTRIBUTES = ( PUBLIC_KEY_ATTR, PRIVATE_KEY_ATTR, ) = ( 'public_key', 'private_key', ) properties_schema = { NAME: properties.Schema(properties.Schema.STRING, _('The name of the key pair.'), required=True, constraints=[constraints.Length(min=1, max=255)]), SAVE_PRIVATE_KEY: properties.Schema( properties.Schema.BOOLEAN, _('True if the system should remember a generated private key; ' 'False otherwise.'), default=False), PUBLIC_KEY: properties.Schema( properties.Schema.STRING, _('The optional public key. This allows users to supply the ' 'public key from a pre-existing key pair. If not supplied, a ' 'new key pair will be generated.')), KEY_TYPE: properties.Schema( properties.Schema.STRING, _('Keypair type. Supported since Nova api version 2.2.'), constraints=[constraints.AllowedValues(['ssh', 'x509'])], support_status=support.SupportStatus(version='8.0.0')), USER: properties.Schema( properties.Schema.STRING, _('ID or name of user to whom to add key-pair. The usage of this ' 'property is limited to being used by administrators only. ' 'Supported since Nova api version 2.10.'), constraints=[constraints.CustomConstraint('keystone.user')], support_status=support.SupportStatus(version='9.0.0')), } attributes_schema = { PUBLIC_KEY_ATTR: attributes.Schema(_('The public key.'), type=attributes.Schema.STRING), PRIVATE_KEY_ATTR: attributes.Schema(_('The private key if it has been saved.'), cache_mode=attributes.Schema.CACHE_NONE, type=attributes.Schema.STRING), } default_client_name = 'nova' entity = 'keypairs' def __init__(self, name, json_snippet, stack): super(KeyPair, self).__init__(name, json_snippet, stack) self._public_key = None def translation_rules(self, props): return [ translation.TranslationRule( props, translation.TranslationRule.RESOLVE, [self.USER], client_plugin=self.client_plugin('keystone'), finder='get_user_id') ] @property def private_key(self): """Return the private SSH key for the resource.""" if self.properties[self.SAVE_PRIVATE_KEY]: return self.data().get('private_key', '') else: return '' @property def public_key(self): """Return the public SSH key for the resource.""" if not self._public_key: if self.properties[self.PUBLIC_KEY]: self._public_key = self.properties[self.PUBLIC_KEY] elif self.resource_id: nova_key = self.client_plugin().get_keypair(self.resource_id) self._public_key = nova_key.public_key return self._public_key def validate(self): super(KeyPair, self).validate() # Check if key_type is allowed to use key_type = self.properties[self.KEY_TYPE] user = self.properties[self.USER] validate_props = [] c_plugin = self.client_plugin() if key_type and not c_plugin.is_version_supported( MICROVERSION_KEY_TYPE): validate_props.append(self.KEY_TYPE) if user and not c_plugin.is_version_supported(MICROVERSION_USER): validate_props.append(self.USER) if validate_props: msg = (_('Cannot use "%s" properties - nova does not ' 'support required api microversion.') % validate_props) raise exception.StackValidationFailed(message=msg) def handle_create(self): pub_key = self.properties[self.PUBLIC_KEY] or None user_id = self.properties[self.USER] key_type = self.properties[self.KEY_TYPE] create_kwargs = { 'name': self.properties[self.NAME], 'public_key': pub_key } if key_type: create_kwargs[self.KEY_TYPE] = key_type if user_id: create_kwargs['user_id'] = user_id new_keypair = self.client().keypairs.create(**create_kwargs) if (self.properties[self.SAVE_PRIVATE_KEY] and hasattr(new_keypair, 'private_key')): self.data_set('private_key', new_keypair.private_key, True) self.resource_id_set(new_keypair.id) def handle_check(self): self.client().keypairs.get(self.resource_id) def _resolve_attribute(self, key): attr_fn = { self.PRIVATE_KEY_ATTR: self.private_key, self.PUBLIC_KEY_ATTR: self.public_key } return six.text_type(attr_fn[key]) def get_reference_id(self): return self.resource_id def prepare_for_replace(self): if self.resource_id is None: return with self.client_plugin().ignore_not_found: self.client().keypairs.delete(self.resource_id)
class CloudNetwork(resource.Resource): """A resource for creating Rackspace Cloud Networks. See http://www.rackspace.com/cloud/networks/ for service documentation. """ support_status = support.SupportStatus( status=support.DEPRECATED, message=_('Use OS::Neutron::Net instead.'), version='2015.1') PROPERTIES = (LABEL, CIDR) = ("label", "cidr") ATTRIBUTES = ( CIDR_ATTR, LABEL_ATTR, ) = ( 'cidr', 'label', ) properties_schema = { LABEL: properties.Schema(properties.Schema.STRING, _("The name of the network."), required=True, constraints=[constraints.Length(min=3, max=64)]), CIDR: properties.Schema( properties.Schema.STRING, _("The IP block from which to allocate the network. For example, " "172.16.0.0/24 or 2001:DB8::/64."), required=True, constraints=[constraints.CustomConstraint('net_cidr')]) } attributes_schema = { CIDR_ATTR: attributes.Schema(_("The CIDR for an isolated private network.")), LABEL_ATTR: attributes.Schema(_("The name of the network.")), } def __init__(self, name, json_snippet, stack): resource.Resource.__init__(self, name, json_snippet, stack) self._network = None def network(self): if self.resource_id and not self._network: try: self._network = self.cloud_networks().get(self.resource_id) except NotFound: LOG.warn( _LW("Could not find network %s but resource id is" " set."), self.resource_id) return self._network def cloud_networks(self): return self.client('cloud_networks') def handle_create(self): cnw = self.cloud_networks().create(label=self.properties[self.LABEL], cidr=self.properties[self.CIDR]) self.resource_id_set(cnw.id) def handle_check(self): self.cloud_networks().get(self.resource_id) def handle_delete(self): '''Delete cloud network. Cloud Network doesn't have a status attribute, and there is a non-zero window between the deletion of a server and the acknowledgement from the cloud network that it's no longer in use, so it needs some way to keep track of when the delete call was successfully issued. ''' network_info = { 'delete_issued': False, 'network': self.network(), } return network_info def check_delete_complete(self, network_info): network = network_info['network'] if not network: return True if not network_info['delete_issued']: try: network.delete() except NetworkInUse: LOG.warn("Network '%s' still in use." % network.id) else: network_info['delete_issued'] = True return False try: network.get() except NotFound: return True return False def validate(self): super(CloudNetwork, self).validate() def _resolve_attribute(self, name): net = self.network() if net: return six.text_type(getattr(net, name)) return ""
class ResourceGroup(stack_resource.StackResource): """Creates one or more identically configured nested resources. In addition to the `refs` attribute, this resource implements synthetic attributes that mirror those of the resources in the group. When getting an attribute from this resource, however, a list of attribute values for each resource in the group is returned. To get attribute values for a single resource in the group, synthetic attributes of the form `resource.{resource index}.{attribute name}` can be used. The resource ID of a particular resource in the group can be obtained via the synthetic attribute `resource.{resource index}`. Note, that if you get attribute without `{resource index}`, e.g. `[resource, {attribute_name}]`, you'll get a list of this attribute's value for all resources in group. While each resource in the group will be identically configured, this resource does allow for some index-based customization of the properties of the resources in the group. For example:: resources: my_indexed_group: type: OS::Heat::ResourceGroup properties: count: 3 resource_def: type: OS::Nova::Server properties: # create a unique name for each server # using its index in the group name: my_server_%index% image: CentOS 6.5 flavor: 4GB Performance would result in a group of three servers having the same image and flavor, but names of `my_server_0`, `my_server_1`, and `my_server_2`. The variable used for substitution can be customized by using the `index_var` property. """ support_status = support.SupportStatus(version='2014.1') PROPERTIES = ( COUNT, INDEX_VAR, RESOURCE_DEF, REMOVAL_POLICIES, REMOVAL_POLICIES_MODE, ) = ('count', 'index_var', 'resource_def', 'removal_policies', 'removal_policies_mode') _RESOURCE_DEF_KEYS = ( RESOURCE_DEF_TYPE, RESOURCE_DEF_PROPERTIES, RESOURCE_DEF_METADATA, ) = ( 'type', 'properties', 'metadata', ) _REMOVAL_POLICIES_KEYS = (REMOVAL_RSRC_LIST, ) = ('resource_list', ) _REMOVAL_POLICY_MODES = (REMOVAL_POLICY_APPEND, REMOVAL_POLICY_UPDATE) = ('append', 'update') _ROLLING_UPDATES_SCHEMA_KEYS = ( MIN_IN_SERVICE, MAX_BATCH_SIZE, PAUSE_TIME, ) = ( 'min_in_service', 'max_batch_size', 'pause_time', ) _BATCH_CREATE_SCHEMA_KEYS = ( MAX_BATCH_SIZE, PAUSE_TIME, ) = ( 'max_batch_size', 'pause_time', ) _UPDATE_POLICY_SCHEMA_KEYS = ( ROLLING_UPDATE, BATCH_CREATE, ) = ( 'rolling_update', 'batch_create', ) ATTRIBUTES = (REFS, REFS_MAP, ATTR_ATTRIBUTES, REMOVED_RSRC_LIST) = ('refs', 'refs_map', 'attributes', 'removed_rsrc_list') properties_schema = { COUNT: properties.Schema(properties.Schema.INTEGER, _('The number of resources to create.'), default=1, constraints=[ constraints.Range(min=0), ], update_allowed=True), INDEX_VAR: properties.Schema( properties.Schema.STRING, _('A variable that this resource will use to replace with the ' 'current index of a given resource in the group. Can be used, ' 'for example, to customize the name property of grouped ' 'servers in order to differentiate them when listed with ' 'nova client.'), default="%index%", constraints=[constraints.Length(min=3)], support_status=support.SupportStatus(version='2014.2')), RESOURCE_DEF: properties.Schema( properties.Schema.MAP, _('Resource definition for the resources in the group. The value ' 'of this property is the definition of a resource just as if ' 'it had been declared in the template itself.'), schema={ RESOURCE_DEF_TYPE: properties.Schema(properties.Schema.STRING, _('The type of the resources in the group.'), required=True), RESOURCE_DEF_PROPERTIES: properties.Schema( properties.Schema.MAP, _('Property values for the resources in the group.')), RESOURCE_DEF_METADATA: properties.Schema( properties.Schema.MAP, _('Supplied metadata for the resources in the group.'), support_status=support.SupportStatus(version='5.0.0')), }, required=True, update_allowed=True), REMOVAL_POLICIES: properties.Schema( properties.Schema.LIST, _('Policies for removal of resources on update.'), schema=properties.Schema( properties.Schema.MAP, _('Policy to be processed when doing an update which ' 'requires removal of specific resources.'), schema={ REMOVAL_RSRC_LIST: properties.Schema( properties.Schema.LIST, _("List of resources to be removed " "when doing an update which requires removal of " "specific resources. " "The resource may be specified several ways: " "(1) The resource name, as in the nested stack, " "(2) The resource reference returned from " "get_resource in a template, as available via " "the 'refs' attribute. " "Note this is destructive on update when specified; " "even if the count is not being reduced, and once " "a resource name is removed, its name is never " "reused in subsequent updates."), default=[]), }, ), update_allowed=True, default=[], support_status=support.SupportStatus(version='2015.1')), REMOVAL_POLICIES_MODE: properties.Schema( properties.Schema.STRING, _('How to handle changes to removal_policies on update. ' 'The default "append" mode appends to the internal list, ' '"update" replaces it on update.'), default=REMOVAL_POLICY_APPEND, constraints=[constraints.AllowedValues(_REMOVAL_POLICY_MODES)], update_allowed=True, support_status=support.SupportStatus(version='10.0.0')), } attributes_schema = { REFS: attributes.Schema( _("A list of resource IDs for the resources in the group."), type=attributes.Schema.LIST), REFS_MAP: attributes.Schema( _("A map of resource names to IDs for the resources in " "the group."), type=attributes.Schema.MAP, support_status=support.SupportStatus(version='7.0.0'), ), ATTR_ATTRIBUTES: attributes.Schema( _("A map of resource names to the specified attribute of each " "individual resource. " "Requires heat_template_version: 2014-10-16."), support_status=support.SupportStatus(version='2014.2'), type=attributes.Schema.MAP), REMOVED_RSRC_LIST: attributes.Schema( _("A list of removed resource names."), support_status=support.SupportStatus(version='7.0.0'), type=attributes.Schema.LIST), } rolling_update_schema = { MIN_IN_SERVICE: properties.Schema(properties.Schema.INTEGER, _('The minimum number of resources in service while ' 'rolling updates are being executed.'), constraints=[constraints.Range(min=0)], default=0), MAX_BATCH_SIZE: properties.Schema( properties.Schema.INTEGER, _('The maximum number of resources to replace at once.'), constraints=[constraints.Range(min=1)], default=1), PAUSE_TIME: properties.Schema(properties.Schema.NUMBER, _('The number of seconds to wait between batches of ' 'updates.'), constraints=[constraints.Range(min=0)], default=0), } batch_create_schema = { MAX_BATCH_SIZE: properties.Schema( properties.Schema.INTEGER, _('The maximum number of resources to create at once.'), constraints=[constraints.Range(min=1)], default=1), PAUSE_TIME: properties.Schema(properties.Schema.NUMBER, _('The number of seconds to wait between batches.'), constraints=[constraints.Range(min=0)], default=0), } update_policy_schema = { ROLLING_UPDATE: properties.Schema( properties.Schema.MAP, schema=rolling_update_schema, support_status=support.SupportStatus(version='5.0.0')), BATCH_CREATE: properties.Schema( properties.Schema.MAP, schema=batch_create_schema, support_status=support.SupportStatus(version='5.0.0')) } def get_size(self): return self.properties.get(self.COUNT) def validate_nested_stack(self): # Only validate the resource definition (which may be a # nested template) if count is non-zero, to enable folks # to disable features via a zero count if they wish if not self.get_size(): return first_name = next(self._resource_names()) test_tmpl = self._assemble_nested([first_name], include_all=True) res_def = next(six.itervalues(test_tmpl.resource_definitions(None))) # make sure we can resolve the nested resource type self.stack.env.get_class_to_instantiate(res_def.resource_type) try: name = "%s-%s" % (self.stack.name, self.name) nested_stack = self._parse_nested_stack(name, test_tmpl, self.child_params()) nested_stack.strict_validate = False nested_stack.validate() except Exception as ex: path = "%s<%s>" % (self.name, self.template_url) raise exception.StackValidationFailed( ex, path=[self.stack.t.RESOURCES, path]) def _current_blacklist(self): db_rsrc_names = self.data().get('name_blacklist') if db_rsrc_names: return db_rsrc_names.split(',') else: return [] def _get_new_blacklist_entries(self, properties, current_blacklist): insp = grouputils.GroupInspector.from_parent_resource(self) # Now we iterate over the removal policies, and update the blacklist # with any additional names for r in properties.get(self.REMOVAL_POLICIES, []): if self.REMOVAL_RSRC_LIST in r: # Tolerate string or int list values for n in r[self.REMOVAL_RSRC_LIST]: str_n = six.text_type(n) if (str_n in current_blacklist or self.resource_id is None or str_n in insp.member_names(include_failed=True)): yield str_n elif isinstance(n, six.string_types): try: refids = self.get_output(self.REFS_MAP) except (exception.NotFound, exception.TemplateOutputError) as op_err: LOG.debug( 'Falling back to resource_by_refid() ' ' due to %s', op_err) rsrc = self.nested().resource_by_refid(n) if rsrc is not None: yield rsrc.name else: if refids is not None: for name, refid in refids.items(): if refid == n: yield name break # Clear output cache from prior to stack update, so we don't get # outdated values after stack update. self._outputs = None def _update_name_blacklist(self, properties): """Resolve the remove_policies to names for removal.""" # To avoid reusing names after removal, we store a comma-separated # blacklist in the resource data - in cases where you want to # overwrite the stored data, removal_policies_mode: update can be used curr_bl = set(self._current_blacklist()) p_mode = properties.get(self.REMOVAL_POLICIES_MODE, self.REMOVAL_POLICY_APPEND) if p_mode == self.REMOVAL_POLICY_UPDATE: init_bl = set() else: init_bl = curr_bl updated_bl = init_bl | set( self._get_new_blacklist_entries(properties, curr_bl)) # If the blacklist has changed, update the resource data if updated_bl != curr_bl: self.data_set('name_blacklist', ','.join(sorted(updated_bl))) def _name_blacklist(self): """Get the list of resource names to blacklist.""" bl = set(self._current_blacklist()) if self.resource_id is None: bl |= set(self._get_new_blacklist_entries(self.properties, bl)) return bl def _resource_names(self, size=None): name_blacklist = self._name_blacklist() if size is None: size = self.get_size() def is_blacklisted(name): return name in name_blacklist candidates = six.moves.map(six.text_type, itertools.count()) return itertools.islice( six.moves.filterfalse(is_blacklisted, candidates), size) def _count_black_listed(self, existing_members): """Return the number of current resource names that are blacklisted.""" return len(self._name_blacklist() & set(existing_members)) def handle_create(self): self._update_name_blacklist(self.properties) if self.update_policy.get(self.BATCH_CREATE) and self.get_size(): batch_create = self.update_policy[self.BATCH_CREATE] max_batch_size = batch_create[self.MAX_BATCH_SIZE] pause_sec = batch_create[self.PAUSE_TIME] checkers = self._replace(0, max_batch_size, pause_sec) if checkers: checkers[0].start() return checkers else: names = self._resource_names() self.create_with_template(self._assemble_nested(names), self.child_params()) def check_create_complete(self, checkers=None): if checkers is None: return super(ResourceGroup, self).check_create_complete() for checker in checkers: if not checker.started(): checker.start() if not checker.step(): return False return True def _run_to_completion(self, template, timeout): updater = self.update_with_template(template, {}, timeout) while not super(ResourceGroup, self).check_update_complete(updater): yield def _run_update(self, total_capacity, max_updates, timeout): template = self._assemble_for_rolling_update(total_capacity, max_updates) return self._run_to_completion(template, timeout) def check_update_complete(self, checkers): for checker in checkers: if not checker.started(): checker.start() if not checker.step(): return False return True def res_def_changed(self, prop_diff): return self.RESOURCE_DEF in prop_diff def handle_update(self, json_snippet, tmpl_diff, prop_diff): if tmpl_diff: # parse update policy if tmpl_diff.update_policy_changed(): up = json_snippet.update_policy(self.update_policy_schema, self.context) self.update_policy = up checkers = [] self.properties = json_snippet.properties(self.properties_schema, self.context) self._update_name_blacklist(self.properties) if prop_diff and self.res_def_changed(prop_diff): updaters = self._try_rolling_update() if updaters: checkers.extend(updaters) if not checkers: resizer = scheduler.TaskRunner( self._run_to_completion, self._assemble_nested(self._resource_names()), self.stack.timeout_mins) checkers.append(resizer) checkers[0].start() return checkers def _attribute_output_name(self, *attr_path): if attr_path[0] == self.REFS: return self.REFS return ', '.join(six.text_type(a) for a in attr_path) def get_attribute(self, key, *path): if key == self.REMOVED_RSRC_LIST: return self._current_blacklist() if key == self.ATTR_ATTRIBUTES and not path: raise exception.InvalidTemplateAttribute(resource=self.name, key=key) is_resource_ref = (key.startswith("resource.") and not path and (len(key.split('.', 2)) == 2)) if is_resource_ref: output_name = self.REFS_MAP else: output_name = self._attribute_output_name(key, *path) if self.resource_id is not None: try: output = self.get_output(output_name) except (exception.NotFound, exception.TemplateOutputError) as op_err: LOG.debug('Falling back to grouputils due to %s', op_err) else: if is_resource_ref: try: target = key.split('.', 2)[1] return output[target] except KeyError: raise exception.NotFound( _("Member '%(mem)s' not " "found in group resource " "'%(grp)s'.") % { 'mem': target, 'grp': self.name }) if key == self.REFS: return attributes.select_from_attribute(output, path) return output if key.startswith("resource."): return grouputils.get_nested_attrs(self, key, False, *path) names = self._resource_names() if key == self.REFS: vals = [grouputils.get_rsrc_id(self, key, False, n) for n in names] return attributes.select_from_attribute(vals, path) if key == self.REFS_MAP: refs_map = { n: grouputils.get_rsrc_id(self, key, False, n) for n in names } return refs_map if key == self.ATTR_ATTRIBUTES: return dict( (n, grouputils.get_rsrc_attr(self, key, False, n, *path)) for n in names) path = [key] + list(path) return [ grouputils.get_rsrc_attr(self, key, False, n, *path) for n in names ] def _nested_output_defns(self, resource_names, get_attr_fn, get_res_fn): for attr in self.referenced_attrs(): if isinstance(attr, six.string_types): key, path = attr, [] else: key, path = attr[0], list(attr[1:]) output_name = self._attribute_output_name(key, *path) value = None if key.startswith("resource."): keycomponents = key.split('.', 2) res_name = keycomponents[1] attr_path = keycomponents[2:] + path if attr_path: if res_name in resource_names: value = get_attr_fn([res_name] + attr_path) else: output_name = key = self.REFS_MAP elif key == self.ATTR_ATTRIBUTES and path: value = {r: get_attr_fn([r] + path) for r in resource_names} elif key not in self.ATTRIBUTES: value = [get_attr_fn([r, key] + path) for r in resource_names] if key == self.REFS: value = [get_res_fn(r) for r in resource_names] if value is not None: yield output.OutputDefinition(output_name, value) value = {r: get_res_fn(r) for r in resource_names} yield output.OutputDefinition(self.REFS_MAP, value) def build_resource_definition(self, res_name, res_defn): res_def = copy.deepcopy(res_defn) props = res_def.get(self.RESOURCE_DEF_PROPERTIES) if props: props = self._handle_repl_val(res_name, props) res_type = res_def[self.RESOURCE_DEF_TYPE] meta = res_def[self.RESOURCE_DEF_METADATA] return rsrc_defn.ResourceDefinition(res_name, res_type, props, meta) def get_resource_def(self, include_all=False): """Returns the resource definition portion of the group. :param include_all: if False, only properties for the resource definition that are not empty will be included :type include_all: bool :return: resource definition for the group :rtype: dict """ # At this stage, we don't mind if all of the parameters have values # assigned. Pass in a custom resolver to the properties to not # error when a parameter does not have a user entered value. def ignore_param_resolve(snippet): if isinstance(snippet, function.Function): try: return snippet.result() except exception.UserParameterMissing: return None if isinstance(snippet, collections.Mapping): return dict( (k, ignore_param_resolve(v)) for k, v in snippet.items()) elif (not isinstance(snippet, six.string_types) and isinstance(snippet, collections.Iterable)): return [ignore_param_resolve(v) for v in snippet] return snippet self.properties.resolve = ignore_param_resolve res_def = self.properties[self.RESOURCE_DEF] if not include_all: return self._clean_props(res_def) return res_def def _clean_props(self, res_defn): res_def = copy.deepcopy(res_defn) props = res_def.get(self.RESOURCE_DEF_PROPERTIES) if props: clean = dict((k, v) for k, v in props.items() if v is not None) props = clean res_def[self.RESOURCE_DEF_PROPERTIES] = props return res_def def _handle_repl_val(self, res_name, val): repl_var = self.properties[self.INDEX_VAR] def recurse(x): return self._handle_repl_val(res_name, x) if isinstance(val, six.string_types): return val.replace(repl_var, res_name) elif isinstance(val, collections.Mapping): return {k: recurse(v) for k, v in val.items()} elif isinstance(val, collections.Sequence): return [recurse(v) for v in val] return val def _add_output_defns_to_template(self, tmpl, resource_names): att_func = 'get_attr' get_attr = functools.partial(tmpl.functions[att_func], None, att_func) res_func = 'get_resource' get_res = functools.partial(tmpl.functions[res_func], None, res_func) for odefn in self._nested_output_defns(resource_names, get_attr, get_res): tmpl.add_output(odefn) def _assemble_nested(self, names, include_all=False, template_version=('heat_template_version', '2015-04-30')): def_dict = self.get_resource_def(include_all) definitions = [(k, self.build_resource_definition(k, def_dict)) for k in names] tmpl = scl_template.make_template(definitions, version=template_version) self._add_output_defns_to_template(tmpl, [k for k, d in definitions]) return tmpl def child_template_files(self, child_env): is_rolling_update = (self.action == self.UPDATE and self.update_policy[self.ROLLING_UPDATE]) return grouputils.get_child_template_files(self.context, self.stack, is_rolling_update, self.old_template_id) def _assemble_for_rolling_update(self, total_capacity, max_updates, include_all=False, template_version=('heat_template_version', '2015-04-30')): names = list(self._resource_names(total_capacity)) name_blacklist = self._name_blacklist() valid_resources = [(n, d) for n, d in grouputils.get_member_definitions(self) if n not in name_blacklist] targ_cap = self.get_size() def replace_priority(res_item): name, defn = res_item try: index = names.index(name) except ValueError: # High priority - delete immediately return 0 else: if index < targ_cap: # Update higher indices first return targ_cap - index else: # Low priority - don't update return total_capacity old_resources = sorted(valid_resources, key=replace_priority) existing_names = set(n for n, d in valid_resources) new_names = six.moves.filterfalse(lambda n: n in existing_names, names) res_def = self.get_resource_def(include_all) definitions = scl_template.member_definitions( old_resources, res_def, total_capacity, max_updates, lambda: next(new_names), self.build_resource_definition) tmpl = scl_template.make_template(definitions, version=template_version) self._add_output_defns_to_template(tmpl, names) return tmpl def _try_rolling_update(self): if self.update_policy[self.ROLLING_UPDATE]: policy = self.update_policy[self.ROLLING_UPDATE] return self._replace(policy[self.MIN_IN_SERVICE], policy[self.MAX_BATCH_SIZE], policy[self.PAUSE_TIME]) def _resolve_attribute(self, name): if name == self.REMOVED_RSRC_LIST: return self._current_blacklist() def _update_timeout(self, batch_cnt, pause_sec): total_pause_time = pause_sec * max(batch_cnt - 1, 0) if total_pause_time >= self.stack.timeout_secs(): msg = _('The current update policy will result in stack update ' 'timeout.') raise ValueError(msg) return self.stack.timeout_secs() - total_pause_time @staticmethod def _get_batches(targ_cap, curr_cap, batch_size, min_in_service): updated = 0 while rolling_update.needs_update(targ_cap, curr_cap, updated): new_cap, total_new = rolling_update.next_batch( targ_cap, curr_cap, updated, batch_size, min_in_service) yield new_cap, total_new updated += total_new - max(new_cap - max(curr_cap, targ_cap), 0) curr_cap = new_cap def _replace(self, min_in_service, batch_size, pause_sec): def pause_between_batch(pause_sec): duration = timeutils.Duration(pause_sec) while not duration.expired(): yield # current capacity not including existing blacklisted inspector = grouputils.GroupInspector.from_parent_resource(self) num_blacklist = self._count_black_listed( inspector.member_names(include_failed=False)) num_resources = inspector.size(include_failed=True) curr_cap = num_resources - num_blacklist batches = list( self._get_batches(self.get_size(), curr_cap, batch_size, min_in_service)) update_timeout = self._update_timeout(len(batches), pause_sec) def tasks(): for index, (curr_cap, max_upd) in enumerate(batches): yield scheduler.TaskRunner(self._run_update, curr_cap, max_upd, update_timeout) if index < (len(batches) - 1) and pause_sec > 0: yield scheduler.TaskRunner(pause_between_batch, pause_sec) return list(tasks()) def preview(self): # NOTE(pas-ha) just need to use include_all in _assemble_nested, # so this method is a simplified copy of preview() from StackResource, # and next two lines are basically a modified copy of child_template() names = self._resource_names() child_template = self._assemble_nested(names, include_all=True) params = self.child_params() name = "%s-%s" % (self.stack.name, self.name) self._nested = self._parse_nested_stack(name, child_template, params) return self.nested().preview_resources() def child_template(self): names = self._resource_names() return self._assemble_nested(names) def child_params(self): return {} def handle_adopt(self, resource_data): names = self._resource_names() if names: return self.create_with_template(self._assemble_nested(names), {}, adopt_data=resource_data) def get_nested_parameters_stack(self): """Return a nested group of size 1 for validation.""" names = self._resource_names(1) child_template = self._assemble_nested(names) params = self.child_params() name = "%s-%s" % (self.stack.name, self.name) return self._parse_nested_stack(name, child_template, params)
class Workflow(signal_responder.SignalResponder, resource.Resource): """A resource that implements Mistral workflow. Workflow represents a process that can be described in a various number of ways and that can do some job interesting to the end user. Each workflow consists of tasks (at least one) describing what exact steps should be made during workflow execution. For detailed description how to use Workflow, read Mistral documentation. """ support_status = support.SupportStatus(version='2015.1') default_client_name = 'mistral' entity = 'workflows' PROPERTIES = (NAME, TYPE, DESCRIPTION, INPUT, OUTPUT, TASKS, PARAMS, TASK_DEFAULTS, USE_REQUEST_BODY_AS_INPUT) = ('name', 'type', 'description', 'input', 'output', 'tasks', 'params', 'task_defaults', 'use_request_body_as_input') _TASKS_KEYS = (TASK_NAME, TASK_DESCRIPTION, ON_ERROR, ON_COMPLETE, ON_SUCCESS, POLICIES, ACTION, WORKFLOW, PUBLISH, TASK_INPUT, REQUIRES, RETRY, WAIT_BEFORE, WAIT_AFTER, PAUSE_BEFORE, TIMEOUT, WITH_ITEMS, KEEP_RESULT, TARGET, JOIN, CONCURRENCY) = ('name', 'description', 'on_error', 'on_complete', 'on_success', 'policies', 'action', 'workflow', 'publish', 'input', 'requires', 'retry', 'wait_before', 'wait_after', 'pause_before', 'timeout', 'with_items', 'keep_result', 'target', 'join', 'concurrency') _TASKS_TASK_DEFAULTS = [ ON_ERROR, ON_COMPLETE, ON_SUCCESS, REQUIRES, RETRY, WAIT_BEFORE, WAIT_AFTER, PAUSE_BEFORE, TIMEOUT, CONCURRENCY ] _SIGNAL_DATA_KEYS = (SIGNAL_DATA_INPUT, SIGNAL_DATA_PARAMS) = ('input', 'params') ATTRIBUTES = (WORKFLOW_DATA, ALARM_URL, EXECUTIONS) = ('data', 'alarm_url', 'executions') properties_schema = { NAME: properties.Schema(properties.Schema.STRING, _('Workflow name.')), TYPE: properties.Schema( properties.Schema.STRING, _('Workflow type.'), constraints=[constraints.AllowedValues(['direct', 'reverse'])], required=True, update_allowed=True), USE_REQUEST_BODY_AS_INPUT: properties.Schema( properties.Schema.BOOLEAN, _('Defines the method in which the request body for signaling a ' 'workflow would be parsed. In case this property is set to ' 'True, the body would be parsed as a simple json where each ' 'key is a workflow input, in other cases body would be parsed ' 'expecting a specific json format with two keys: "input" and ' '"params".'), update_allowed=True, support_status=support.SupportStatus(version='6.0.0')), DESCRIPTION: properties.Schema(properties.Schema.STRING, _('Workflow description.'), update_allowed=True), INPUT: properties.Schema(properties.Schema.MAP, _('Dictionary which contains input for workflow.'), update_allowed=True), OUTPUT: properties.Schema(properties.Schema.MAP, _('Any data structure arbitrarily containing YAQL ' 'expressions that defines workflow output. May be ' 'nested.'), update_allowed=True), PARAMS: properties.Schema( properties.Schema.MAP, _("Workflow additional parameters. If Workflow is reverse typed, " "params requires 'task_name', which defines initial task."), update_allowed=True), TASK_DEFAULTS: properties.Schema( properties.Schema.MAP, _("Default settings for some of task " "attributes defined " "at workflow level."), support_status=support.SupportStatus(version='5.0.0'), schema={ ON_SUCCESS: properties.Schema( properties.Schema.LIST, _('List of tasks which will run after ' 'the task has completed successfully.')), ON_ERROR: properties.Schema( properties.Schema.LIST, _('List of tasks which will run after ' 'the task has completed with an error.')), ON_COMPLETE: properties.Schema( properties.Schema.LIST, _('List of tasks which will run after ' 'the task has completed regardless of whether ' 'it is successful or not.')), REQUIRES: properties.Schema( properties.Schema.LIST, _('List of tasks which should be executed before ' 'this task. Used only in reverse workflows.')), RETRY: properties.Schema( properties.Schema.MAP, _('Defines a pattern how task should be repeated in ' 'case of an error.')), WAIT_BEFORE: properties.Schema( properties.Schema.INTEGER, _('Defines a delay in seconds that Mistral Engine ' 'should wait before starting a task.')), WAIT_AFTER: properties.Schema( properties.Schema.INTEGER, _('Defines a delay in seconds that Mistral Engine ' 'should wait after a task has completed before ' 'starting next tasks defined in ' 'on-success, on-error or on-complete.')), PAUSE_BEFORE: properties.Schema( properties.Schema.BOOLEAN, _('Defines whether Mistral Engine should put the ' 'workflow on hold or not before starting a task.')), TIMEOUT: properties.Schema( properties.Schema.INTEGER, _('Defines a period of time in seconds after which ' 'a task will be failed automatically ' 'by engine if hasn\'t completed.')), CONCURRENCY: properties.Schema( properties.Schema.INTEGER, _('Defines a max number of actions running simultaneously ' 'in a task. Applicable only for tasks that have ' 'with-items.'), support_status=support.SupportStatus(version='8.0.0')) }, update_allowed=True), TASKS: properties.Schema( properties.Schema.LIST, _('Dictionary containing workflow tasks.'), schema=properties.Schema( properties.Schema.MAP, schema={ TASK_NAME: properties.Schema(properties.Schema.STRING, _('Task name.'), required=True), TASK_DESCRIPTION: properties.Schema(properties.Schema.STRING, _('Task description.')), TASK_INPUT: properties.Schema( properties.Schema.MAP, _('Actual input parameter values of the task.')), ACTION: properties.Schema( properties.Schema.STRING, _('Name of the action associated with the task. ' 'Either action or workflow may be defined in the ' 'task.')), WORKFLOW: properties.Schema( properties.Schema.STRING, _('Name of the workflow associated with the task. ' 'Can be defined by intrinsic function get_resource ' 'or by name of the referenced workflow, i.e. ' '{ workflow: wf_name } or ' '{ workflow: { get_resource: wf_name }}. Either ' 'action or workflow may be defined in the task.'), constraints=[ constraints.CustomConstraint('mistral.workflow') ]), PUBLISH: properties.Schema( properties.Schema.MAP, _('Dictionary of variables to publish to ' 'the workflow context.')), ON_SUCCESS: properties.Schema( properties.Schema.LIST, _('List of tasks which will run after ' 'the task has completed successfully.')), ON_ERROR: properties.Schema( properties.Schema.LIST, _('List of tasks which will run after ' 'the task has completed with an error.')), ON_COMPLETE: properties.Schema( properties.Schema.LIST, _('List of tasks which will run after ' 'the task has completed regardless of whether ' 'it is successful or not.')), POLICIES: properties.Schema( properties.Schema.MAP, _('Dictionary-like section defining task policies ' 'that influence how Mistral Engine runs tasks. Must ' 'satisfy Mistral DSL v2.'), support_status=support.SupportStatus( status=support.HIDDEN, version='8.0.0', message=_('Add needed policies directly to ' 'the task, Policy keyword is not ' 'needed'), previous_status=support.SupportStatus( status=support.DEPRECATED, version='5.0.0', previous_status=support.SupportStatus( version='2015.1')))), REQUIRES: properties.Schema( properties.Schema.LIST, _('List of tasks which should be executed before ' 'this task. Used only in reverse workflows.')), RETRY: properties.Schema( properties.Schema.MAP, _('Defines a pattern how task should be repeated in ' 'case of an error.'), support_status=support.SupportStatus(version='5.0.0')), WAIT_BEFORE: properties.Schema( properties.Schema.INTEGER, _('Defines a delay in seconds that Mistral Engine ' 'should wait before starting a task.'), support_status=support.SupportStatus(version='5.0.0')), WAIT_AFTER: properties.Schema( properties.Schema.INTEGER, _('Defines a delay in seconds that Mistral ' 'Engine should wait after ' 'a task has completed before starting next tasks ' 'defined in on-success, on-error or on-complete.'), support_status=support.SupportStatus(version='5.0.0')), PAUSE_BEFORE: properties.Schema( properties.Schema.BOOLEAN, _('Defines whether Mistral Engine should ' 'put the workflow on hold ' 'or not before starting a task.'), support_status=support.SupportStatus(version='5.0.0')), TIMEOUT: properties.Schema( properties.Schema.INTEGER, _('Defines a period of time in seconds after which a ' 'task will be failed automatically by engine ' 'if hasn\'t completed.'), support_status=support.SupportStatus(version='5.0.0')), WITH_ITEMS: properties.Schema( properties.Schema.STRING, _('If configured, it allows to run action or workflow ' 'associated with a task multiple times ' 'on a provided list of items.'), support_status=support.SupportStatus(version='5.0.0')), KEEP_RESULT: properties.Schema( properties.Schema.BOOLEAN, _('Allowing not to store action results ' 'after task completion.'), support_status=support.SupportStatus(version='5.0.0')), CONCURRENCY: properties.Schema( properties.Schema.INTEGER, _('Defines a max number of actions running ' 'simultaneously in a task. Applicable only for ' 'tasks that have with-items.'), support_status=support.SupportStatus(version='8.0.0')), TARGET: properties.Schema( properties.Schema.STRING, _('It defines an executor to which task action ' 'should be sent to.'), support_status=support.SupportStatus(version='5.0.0')), JOIN: properties.Schema( properties.Schema.STRING, _('Allows to synchronize multiple parallel workflow ' 'branches and aggregate their data. ' 'Valid inputs: all - the task will run only if ' 'all upstream tasks are completed. ' 'Any numeric value - then the task will run once ' 'at least this number of upstream tasks are ' 'completed and corresponding conditions have ' 'triggered.'), support_status=support.SupportStatus(version='6.0.0')), }, ), required=True, update_allowed=True, constraints=[constraints.Length(min=1)]) } attributes_schema = { WORKFLOW_DATA: attributes.Schema( _('A dictionary which contains name and input of the workflow.'), type=attributes.Schema.MAP), ALARM_URL: attributes.Schema(_( "A signed url to create executions for workflows specified in " "Workflow resource."), type=attributes.Schema.STRING), EXECUTIONS: attributes.Schema(_( "List of workflows' executions, each of them is a dictionary " "with information about execution. Each dictionary returns " "values for next keys: id, workflow_name, created_at, " "updated_at, state for current execution state, input, output."), type=attributes.Schema.LIST) } def translation_rules(self, properties): policies_keys = [ self.PAUSE_BEFORE, self.WAIT_AFTER, self.WAIT_BEFORE, self.TIMEOUT, self.CONCURRENCY, self.RETRY ] rules = [] for key in policies_keys: rules.append( translation.TranslationRule( properties, translation.TranslationRule.REPLACE, [self.TASKS, key], value_name=self.POLICIES, custom_value_path=[key])) # after executing rules above properties data contains policies key # with empty dict value, so need to remove policies from properties. rules.append( translation.TranslationRule(properties, translation.TranslationRule.DELETE, [self.TASKS, self.POLICIES])) return rules def get_reference_id(self): return self._workflow_name() def _get_inputs_and_params(self, data): inputs = None params = None if self.properties.get(self.USE_REQUEST_BODY_AS_INPUT): inputs = data else: if data is not None: inputs = data.get(self.SIGNAL_DATA_INPUT) params = data.get(self.SIGNAL_DATA_PARAMS) return inputs, params def _validate_signal_data(self, inputs, params): if inputs is not None: if not isinstance(inputs, dict): message = (_('Input in signal data must be a map, ' 'find a %s') % type(inputs)) raise exception.StackValidationFailed( error=_('Signal data error'), message=message) for key in inputs: if (self.properties.get(self.INPUT) is None or key not in self.properties.get(self.INPUT)): message = _('Unknown input %s') % key raise exception.StackValidationFailed( error=_('Signal data error'), message=message) if params is not None and not isinstance(params, dict): message = (_('Params must be a map, find a ' '%s') % type(params)) raise exception.StackValidationFailed(error=_('Signal data error'), message=message) def validate(self): super(Workflow, self).validate() if self.properties.get(self.TYPE) == 'reverse': params = self.properties.get(self.PARAMS) if params is None or not params.get('task_name'): raise exception.StackValidationFailed( error=_('Mistral resource validation error'), path=[ self.name, ('properties' if self.stack.t.VERSION == 'heat_template_version' else 'Properties'), self.PARAMS ], message=_("'task_name' is not assigned in 'params' " "in case of reverse type workflow.")) for task in self.properties.get(self.TASKS): wf_value = task.get(self.WORKFLOW) action_value = task.get(self.ACTION) if wf_value and action_value: raise exception.ResourcePropertyConflict( self.WORKFLOW, self.ACTION) if not wf_value and not action_value: raise exception.PropertyUnspecifiedError( self.WORKFLOW, self.ACTION) if (task.get(self.REQUIRES) is not None and self.properties.get(self.TYPE)) == 'direct': msg = _("task %(task)s contains property 'requires' " "in case of direct workflow. Only reverse workflows " "can contain property 'requires'.") % { 'name': self.name, 'task': task.get(self.TASK_NAME) } raise exception.StackValidationFailed( error=_('Mistral resource validation error'), path=[ self.name, ('properties' if self.stack.t.VERSION == 'heat_template_version' else 'Properties'), self.TASKS, task.get(self.TASK_NAME), self.REQUIRES ], message=msg) if task.get(self.POLICIES) is not None: for task_item in task.get(self.POLICIES): if task.get(task_item) is not None: msg = _('Property %(policies)s and %(item)s cannot be ' 'used both at one time.') % { 'policies': self.POLICIES, 'item': task_item } raise exception.StackValidationFailed(message=msg) if (task.get(self.WITH_ITEMS) is None and task.get(self.CONCURRENCY) is not None): raise exception.ResourcePropertyDependency( prop1=self.CONCURRENCY, prop2=self.WITH_ITEMS) def _workflow_name(self): return self.properties.get(self.NAME) or self.physical_resource_name() def build_tasks(self, props): for task in props[self.TASKS]: current_task = {} wf_value = task.get(self.WORKFLOW) if wf_value is not None: current_task.update({self.WORKFLOW: wf_value}) # backward support for kilo. if task.get(self.POLICIES) is not None: task.update(task.get(self.POLICIES)) task_keys = [ key for key in self._TASKS_KEYS if key not in [self.WORKFLOW, self.TASK_NAME, self.POLICIES] ] for task_prop in task_keys: if task.get(task_prop) is not None: current_task.update( {task_prop.replace('_', '-'): task[task_prop]}) yield {task[self.TASK_NAME]: current_task} def prepare_properties(self, props): """Prepare correct YAML-formatted definition for Mistral.""" defn_name = self._workflow_name() definition = { 'version': '2.0', defn_name: { self.TYPE: props.get(self.TYPE), self.DESCRIPTION: props.get(self.DESCRIPTION), self.OUTPUT: props.get(self.OUTPUT) } } for key in list(definition[defn_name].keys()): if definition[defn_name][key] is None: del definition[defn_name][key] if props.get(self.INPUT) is not None: definition[defn_name][self.INPUT] = list( props.get(self.INPUT).keys()) definition[defn_name][self.TASKS] = {} for task in self.build_tasks(props): definition.get(defn_name).get(self.TASKS).update(task) if props.get(self.TASK_DEFAULTS) is not None: definition[defn_name][self.TASK_DEFAULTS.replace('_', '-')] = { k.replace('_', '-'): v for k, v in six.iteritems(props.get(self.TASK_DEFAULTS)) if v } return yaml.dump(definition, Dumper=yaml.CSafeDumper if hasattr( yaml, 'CSafeDumper') else yaml.SafeDumper) def handle_create(self): super(Workflow, self).handle_create() props = self.prepare_properties(self.properties) try: workflow = self.client().workflows.create(props) except Exception as ex: raise exception.ResourceFailure(ex, self) # NOTE(prazumovsky): Mistral uses unique names for resource # identification. self.resource_id_set(workflow[0].name) def handle_signal(self, details=None): inputs, params = self._get_inputs_and_params(details) self._validate_signal_data(inputs, params) inputs_result = copy.deepcopy(self.properties[self.INPUT]) params_result = copy.deepcopy(self.properties[self.PARAMS]) or {} # NOTE(prazumovsky): Signal can contains some data, interesting # for workflow, e.g. inputs. So, if signal data contains input # we update override inputs, other leaved defined in template. if inputs: inputs_result.update(inputs) if params: params_result.update(params) try: execution = self.client().executions.create( self._workflow_name(), jsonutils.dumps(inputs_result), **params_result) except Exception as ex: raise exception.ResourceFailure(ex, self) executions = [execution.id] if self.EXECUTIONS in self.data(): executions.extend(self.data().get(self.EXECUTIONS).split(',')) self.data_set(self.EXECUTIONS, ','.join(executions)) def handle_update(self, json_snippet, tmpl_diff, prop_diff): if prop_diff: props = json_snippet.properties(self.properties_schema, self.context) new_props = self.prepare_properties(props) try: workflow = self.client().workflows.update(new_props) except Exception as ex: raise exception.ResourceFailure(ex, self) self.data_set(self.NAME, workflow[0].name) self.resource_id_set(workflow[0].name) def _delete_executions(self): if self.data().get(self.EXECUTIONS): for id in self.data().get(self.EXECUTIONS).split(','): with self.client_plugin().ignore_not_found: self.client().executions.delete(id) self.data_delete('executions') def handle_delete(self): self._delete_executions() return super(Workflow, self).handle_delete() def _resolve_attribute(self, name): if name == self.EXECUTIONS: if self.EXECUTIONS not in self.data(): return [] def parse_execution_response(execution): return { 'id': execution.id, 'workflow_name': execution.workflow_name, 'created_at': execution.created_at, 'updated_at': execution.updated_at, 'state': execution.state, 'input': jsonutils.loads(six.text_type(execution.input)), 'output': jsonutils.loads(six.text_type(execution.output)) } return [ parse_execution_response(self.client().executions.get(exec_id)) for exec_id in self.data().get(self.EXECUTIONS).split(',') ] elif name == self.WORKFLOW_DATA: return { self.NAME: self.resource_id, self.INPUT: self.properties.get(self.INPUT) } elif name == self.ALARM_URL and self.resource_id is not None: return six.text_type(self._get_ec2_signed_url())
class DesignateDomain(resource.Resource): """Heat Template Resource for Designate Domain. Designate provides DNS-as-a-Service services for OpenStack. So, domain is a realm with an identification string, unique in DNS. """ support_status = support.SupportStatus(version='5.0.0') entity = 'domains' PROPERTIES = (NAME, TTL, DESCRIPTION, EMAIL) = ('name', 'ttl', 'description', 'email') ATTRIBUTES = (SERIAL, ) = ('serial', ) properties_schema = { # Based on RFC 1035, length of name is set to max of 255 NAME: properties.Schema(properties.Schema.STRING, _('Domain name.'), required=True, constraints=[constraints.Length(max=255)]), # Based on RFC 1035, range for ttl is set to 1 to signed 32 bit number TTL: properties.Schema( properties.Schema.INTEGER, _('Time To Live (Seconds).'), update_allowed=True, constraints=[constraints.Range(min=1, max=2147483647)]), # designate mandates to the max length of 160 for description DESCRIPTION: properties.Schema(properties.Schema.STRING, _('Description of domain.'), update_allowed=True, constraints=[constraints.Length(max=160)]), EMAIL: properties.Schema(properties.Schema.STRING, _('Domain email.'), update_allowed=True, required=True) } attributes_schema = { SERIAL: attributes.Schema(_("DNS domain serial."), type=attributes.Schema.STRING), } default_client_name = 'designate' entity = 'domains' def handle_create(self): args = dict((k, v) for k, v in six.iteritems(self.properties) if v) domain = self.client_plugin().domain_create(**args) self.resource_id_set(domain.id) def handle_update(self, json_snippet, tmpl_diff, prop_diff): args = dict() if prop_diff.get(self.EMAIL): args['email'] = prop_diff.get(self.EMAIL) if prop_diff.get(self.TTL): args['ttl'] = prop_diff.get(self.TTL) if prop_diff.get(self.DESCRIPTION): args['description'] = prop_diff.get(self.DESCRIPTION) if len(args.keys()) > 0: args['id'] = self.resource_id self.client_plugin().domain_update(**args) def _resolve_attribute(self, name): if self.resource_id is None: return if name == self.SERIAL: domain = self.client().domains.get(self.resource_id) return domain.serial # FIXME(kanagaraj-manickam) Remove this method once designate defect # 1485552 is fixed. def _show_resource(self): return dict(self.client().domains.get(self.resource_id).items()) def parse_live_resource_data(self, resource_properties, resource_data): domain_reality = {} for key in self.PROPERTIES: domain_reality.update({key: resource_data.get(key)}) return domain_reality