diff --git a/devstack/local.conf.sample b/devstack/local.conf.sample index ec5959f1..4e439fb2 100644 --- a/devstack/local.conf.sample +++ b/devstack/local.conf.sample @@ -37,14 +37,11 @@ enable_plugin tricircle https://github.com/openstack/tricircle/ # Tricircle Services enable_service t-api -enable_service t-ngw -enable_service t-cgw enable_service t-job # Use Neutron instead of nova-network disable_service n-net enable_service q-svc -enable_service q-svc1 enable_service q-dhcp enable_service q-agt @@ -58,3 +55,22 @@ enable_service c-sch disable_service c-bak # disable_service tempest disable_service horizon + +CENTRAL_REGION_NAME=CentralRegion +TRICIRCLE_NEUTRON_PORT=20001 + +[[post-config|$NEUTRON_CONF]] + +[DEFAULT] +core_plugin=tricircle.network.local_plugin.TricirclePlugin + +[client] +admin_username=admin +admin_password=$ADMIN_PASSWORD +admin_tenant=demo +auto_refresh_endpoint=True +top_pod_name=$CENTRAL_REGION_NAME + +[tricircle] +real_core_plugin=neutron.plugins.ml2.plugin.Ml2Plugin +central_neutron_url=http://127.0.0.1:$TRICIRCLE_NEUTRON_PORT diff --git a/devstack/plugin.sh b/devstack/plugin.sh index d62950b5..22c335a5 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -19,9 +19,9 @@ function create_tricircle_accounts { if [[ "$KEYSTONE_CATALOG_BACKEND" = 'sql' ]]; then local tricircle_api=$(get_or_create_service "tricircle" \ - "Cascading" "OpenStack Cascading Service") + "Tricircle" "Cross Neutron Networking Automation Service") get_or_create_endpoint $tricircle_api \ - "$REGION_NAME" \ + "$CENTRAL_REGION_NAME" \ "$SERVICE_PROTOCOL://$TRICIRCLE_API_HOST:$TRICIRCLE_API_PORT/v1.0" \ "$SERVICE_PROTOCOL://$TRICIRCLE_API_HOST:$TRICIRCLE_API_PORT/v1.0" \ "$SERVICE_PROTOCOL://$TRICIRCLE_API_HOST:$TRICIRCLE_API_PORT/v1.0" @@ -29,79 +29,6 @@ function create_tricircle_accounts { fi } -# create_nova_apigw_accounts() - Set up common required nova_apigw -# work as nova api serice -# service accounts in keystone -# Project User Roles -# ----------------------------------------------------------------- -# $SERVICE_TENANT_NAME nova_apigw service - -function create_nova_apigw_accounts { - if [[ "$ENABLED_SERVICES" =~ "t-ngw" ]]; then - create_service_user "nova_apigw" - - if [[ "$KEYSTONE_CATALOG_BACKEND" = 'sql' ]]; then - local tricircle_nova_apigw=$(get_or_create_service "nova" \ - "compute" "Nova Compute Service") - - remove_old_endpoint_conf $tricircle_nova_apigw - - get_or_create_endpoint $tricircle_nova_apigw \ - "$REGION_NAME" \ - "$SERVICE_PROTOCOL://$TRICIRCLE_NOVA_APIGW_HOST:$TRICIRCLE_NOVA_APIGW_PORT/v2.1/"'$(tenant_id)s' \ - "$SERVICE_PROTOCOL://$TRICIRCLE_NOVA_APIGW_HOST:$TRICIRCLE_NOVA_APIGW_PORT/v2.1/"'$(tenant_id)s' \ - "$SERVICE_PROTOCOL://$TRICIRCLE_NOVA_APIGW_HOST:$TRICIRCLE_NOVA_APIGW_PORT/v2.1/"'$(tenant_id)s' - fi - fi -} - -# create_cinder_apigw_accounts() - Set up common required cinder_apigw -# work as cinder api serice -# service accounts in keystone -# Project User Roles -# --------------------------------------------------------------------- -# $SERVICE_TENANT_NAME cinder_apigw service - -function create_cinder_apigw_accounts { - if [[ "$ENABLED_SERVICES" =~ "t-cgw" ]]; then - create_service_user "cinder_apigw" - - if [[ "$KEYSTONE_CATALOG_BACKEND" = 'sql' ]]; then - local tricircle_cinder_apigw=$(get_or_create_service "cinder" \ - "volumev2" "Cinder Volume Service") - - remove_old_endpoint_conf $tricircle_cinder_apigw - - get_or_create_endpoint $tricircle_cinder_apigw \ - "$REGION_NAME" \ - "$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/"'$(tenant_id)s' \ - "$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/"'$(tenant_id)s' \ - "$SERVICE_PROTOCOL://$TRICIRCLE_CINDER_APIGW_HOST:$TRICIRCLE_CINDER_APIGW_PORT/v2/"'$(tenant_id)s' - fi - fi -} - - -# common config-file configuration for tricircle services -function remove_old_endpoint_conf { - local service=$1 - - local endpoint_id - interface_list="public admin internal" - for interface in $interface_list; do - endpoint_id=$(openstack endpoint list \ - --service "$service" \ - --interface "$interface" \ - --region "$REGION_NAME" \ - -c ID -f value) - if [[ -n "$endpoint_id" ]]; then - # Delete endpoint - openstack endpoint delete "$endpoint_id" - fi - done -} - - # create_tricircle_cache_dir() - Set up cache dir for tricircle function create_tricircle_cache_dir { @@ -125,7 +52,7 @@ function init_common_tricircle_conf { iniset $conf_file client admin_password $ADMIN_PASSWORD iniset $conf_file client admin_tenant demo iniset $conf_file client auto_refresh_endpoint True - iniset $conf_file client top_pod_name $REGION_NAME + iniset $conf_file client top_pod_name $CENTRAL_REGION_NAME iniset $conf_file oslo_concurrency lock_path $TRICIRCLE_STATE_PATH/lock } @@ -154,66 +81,21 @@ function configure_tricircle_api { fi } -function configure_tricircle_nova_apigw { - if is_service_enabled t-ngw ; then - echo "Configuring Tricircle Nova APIGW" - - init_common_tricircle_conf $TRICIRCLE_NOVA_APIGW_CONF - - setup_colorized_logging $TRICIRCLE_NOVA_APIGW_CONF DEFAULT tenant_name - - if is_service_enabled keystone; then - - create_tricircle_cache_dir - - # Configure auth token middleware - configure_auth_token_middleware $TRICIRCLE_NOVA_APIGW_CONF tricircle \ - $TRICIRCLE_AUTH_CACHE_DIR - - else - iniset $TRICIRCLE_NOVA_APIGW_CONF DEFAULT auth_strategy noauth - fi - - fi -} - -function configure_tricircle_cinder_apigw { - if is_service_enabled t-cgw ; then - echo "Configuring Tricircle Cinder APIGW" - - init_common_tricircle_conf $TRICIRCLE_CINDER_APIGW_CONF - - setup_colorized_logging $TRICIRCLE_CINDER_APIGW_CONF DEFAULT tenant_name - - if is_service_enabled keystone; then - - create_tricircle_cache_dir - - # Configure auth token middleware - configure_auth_token_middleware $TRICIRCLE_CINDER_APIGW_CONF tricircle \ - $TRICIRCLE_AUTH_CACHE_DIR - - else - iniset $TRICIRCLE_CINDER_APIGW_CONF DEFAULT auth_strategy noauth - fi - - fi -} - function configure_tricircle_xjob { if is_service_enabled t-job ; then echo "Configuring Tricircle xjob" init_common_tricircle_conf $TRICIRCLE_XJOB_CONF + iniset $TRICIRCLE_XJOB_CONF DEFAULT enable_api_gateway False setup_colorized_logging $TRICIRCLE_XJOB_CONF DEFAULT fi } -function start_new_neutron_server { - local server_index=$1 - local region_name=$2 - local q_port=$3 +function start_central_neutron_server { + local server_index=0 + local region_name=$1 + local q_port=$2 get_or_create_service "neutron" "network" "Neutron Service" get_or_create_endpoint "network" \ @@ -222,10 +104,30 @@ function start_new_neutron_server { "$Q_PROTOCOL://$SERVICE_HOST:$q_port/" \ "$Q_PROTOCOL://$SERVICE_HOST:$q_port/" + # reconfigure central neutron server to use our own central plugin + echo "Configuring central Neutron plugin for Tricircle" + cp $NEUTRON_CONF $NEUTRON_CONF.$server_index iniset $NEUTRON_CONF.$server_index database connection `database_connection_url $Q_DB_NAME$server_index` - iniset $NEUTRON_CONF.$server_index nova region_name $region_name iniset $NEUTRON_CONF.$server_index DEFAULT bind_port $q_port + iniset $NEUTRON_CONF.$server_index DEFAULT core_plugin "tricircle.network.central_plugin.TricirclePlugin" + iniset $NEUTRON_CONF.$server_index DEFAULT service_plugins "" + iniset $NEUTRON_CONF.$server_index DEFAULT tricircle_db_connection `database_connection_url tricircle` + iniset $NEUTRON_CONF.$server_index DEFAULT notify_nova_on_port_data_changes False + iniset $NEUTRON_CONF.$server_index DEFAULT notify_nova_on_port_status_changes False + iniset $NEUTRON_CONF.$server_index client admin_username admin + iniset $NEUTRON_CONF.$server_index client admin_password $ADMIN_PASSWORD + iniset $NEUTRON_CONF.$server_index client admin_tenant demo + iniset $NEUTRON_CONF.$server_index client auto_refresh_endpoint True + iniset $NEUTRON_CONF.$server_index client top_pod_name $CENTRAL_REGION_NAME + + if [ "$Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS" != "" ]; then + iniset $NEUTRON_CONF.$server_index tricircle type_drivers local,shared_vlan + iniset $NEUTRON_CONF.$server_index tricircle tenant_network_types local,shared_vlan + iniset $NEUTRON_CONF.$server_index tricircle network_vlan_ranges `echo $Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS | awk -F= '{print $2}'` + iniset $NEUTRON_CONF.$server_index tricircle bridge_network_type shared_vlan + iniset $NEUTRON_CONF.$server_index tricircle enable_api_gateway False + fi recreate_database $Q_DB_NAME$server_index $NEUTRON_BIN_DIR/neutron-db-manage --config-file $NEUTRON_CONF.$server_index --config-file /$Q_PLUGIN_CONF_FILE upgrade head @@ -245,11 +147,9 @@ if [[ "$Q_ENABLE_TRICIRCLE" == "True" ]]; then export NEUTRON_CREATE_INITIAL_NETWORKS=False sudo install -d -o $STACK_USER -m 755 $TRICIRCLE_CONF_DIR - enable_service t-api t-ngw t-cgw t-job + enable_service t-api t-job configure_tricircle_api - configure_tricircle_nova_apigw - configure_tricircle_cinder_apigw configure_tricircle_xjob echo export PYTHONPATH=\$PYTHONPATH:$TRICIRCLE_DIR >> $RC_DIR/.localrc.auto @@ -260,29 +160,7 @@ if [[ "$Q_ENABLE_TRICIRCLE" == "True" ]]; then python "$TRICIRCLE_DIR/cmd/manage.py" "$TRICIRCLE_API_CONF" if is_service_enabled q-svc ; then - start_new_neutron_server 1 $POD_REGION_NAME $TRICIRCLE_NEUTRON_PORT - - # reconfigure neutron server to use our own plugin - echo "Configuring Neutron plugin for Tricircle" - Q_PLUGIN_CLASS="tricircle.network.plugin.TricirclePlugin" - - iniset $NEUTRON_CONF DEFAULT core_plugin "$Q_PLUGIN_CLASS" - iniset $NEUTRON_CONF DEFAULT service_plugins "" - iniset $NEUTRON_CONF DEFAULT tricircle_db_connection `database_connection_url tricircle` - iniset $NEUTRON_CONF DEFAULT notify_nova_on_port_data_changes False - iniset $NEUTRON_CONF DEFAULT notify_nova_on_port_status_changes False - iniset $NEUTRON_CONF client admin_username admin - iniset $NEUTRON_CONF client admin_password $ADMIN_PASSWORD - iniset $NEUTRON_CONF client admin_tenant demo - iniset $NEUTRON_CONF client auto_refresh_endpoint True - iniset $NEUTRON_CONF client top_pod_name $REGION_NAME - - if [ "$Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS" != "" ]; then - iniset $NEUTRON_CONF tricircle type_drivers local,shared_vlan - iniset $NEUTRON_CONF tricircle tenant_network_types local,shared_vlan - iniset $NEUTRON_CONF tricircle network_vlan_ranges `echo $Q_ML2_PLUGIN_VLAN_TYPE_OPTIONS | awk -F= '{print $2}'` - iniset $NEUTRON_CONF tricircle bridge_network_type shared_vlan - fi + start_central_neutron_server $CENTRAL_REGION_NAME $TRICIRCLE_NEUTRON_PORT fi elif [[ "$1" == "stack" && "$2" == "extra" ]]; then @@ -295,47 +173,6 @@ if [[ "$Q_ENABLE_TRICIRCLE" == "True" ]]; then run_process t-api "python $TRICIRCLE_API --config-file $TRICIRCLE_API_CONF" fi - if is_service_enabled t-ngw; then - - create_nova_apigw_accounts - - run_process t-ngw "python $TRICIRCLE_NOVA_APIGW --config-file $TRICIRCLE_NOVA_APIGW_CONF" - - # Nova services are running, but we need to re-configure them to - # move them to bottom region - iniset $NOVA_CONF neutron region_name $POD_REGION_NAME - iniset $NOVA_CONF neutron url "$Q_PROTOCOL://$SERVICE_HOST:$TRICIRCLE_NEUTRON_PORT" - iniset $NOVA_CONF cinder os_region_name $POD_REGION_NAME - - get_or_create_endpoint "compute" \ - "$POD_REGION_NAME" \ - "$NOVA_SERVICE_PROTOCOL://$NOVA_SERVICE_HOST:$NOVA_SERVICE_PORT/v2.1/"'$(tenant_id)s' \ - "$NOVA_SERVICE_PROTOCOL://$NOVA_SERVICE_HOST:$NOVA_SERVICE_PORT/v2.1/"'$(tenant_id)s' \ - "$NOVA_SERVICE_PROTOCOL://$NOVA_SERVICE_HOST:$NOVA_SERVICE_PORT/v2.1/"'$(tenant_id)s' - - stop_process n-api - stop_process n-cpu - # remove previous failure flag file since we are going to restart service - rm -f "$SERVICE_DIR/$SCREEN_NAME"/n-api.failure - rm -f "$SERVICE_DIR/$SCREEN_NAME"/n-cpu.failure - sleep 20 - run_process n-api "$NOVA_BIN_DIR/nova-api" - run_process n-cpu "$NOVA_BIN_DIR/nova-compute --config-file $NOVA_CONF" $LIBVIRT_GROUP - fi - - if is_service_enabled t-cgw; then - - create_cinder_apigw_accounts - - run_process t-cgw "python $TRICIRCLE_CINDER_APIGW --config-file $TRICIRCLE_CINDER_APIGW_CONF" - - get_or_create_endpoint "volumev2" \ - "$POD_REGION_NAME" \ - "$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST:$CINDER_SERVICE_PORT/v2/"'$(tenant_id)s' \ - "$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST:$CINDER_SERVICE_PORT/v2/"'$(tenant_id)s' \ - "$CINDER_SERVICE_PROTOCOL://$CINDER_SERVICE_HOST:$CINDER_SERVICE_PORT/v2/"'$(tenant_id)s' - fi - if is_service_enabled t-job; then run_process t-job "python $TRICIRCLE_XJOB --config-file $TRICIRCLE_XJOB_CONF" @@ -348,20 +185,12 @@ if [[ "$Q_ENABLE_TRICIRCLE" == "True" ]]; then stop_process t-api fi - if is_service_enabled t-ngw; then - stop_process t-ngw - fi - - if is_service_enabled t-cgw; then - stop_process t-cgw - fi - if is_service_enabled t-job; then stop_process t-job fi - if is_service_enabled q-svc1; then - stop_process q-svc1 + if is_service_enabled q-svc0; then + stop_process q-svc0 fi fi fi diff --git a/devstack/settings b/devstack/settings index a404215c..cbdec2ca 100644 --- a/devstack/settings +++ b/devstack/settings @@ -4,7 +4,7 @@ TRICIRCLE_DIR=$DEST/tricircle TRICIRCLE_BRANCH=${TRICIRCLE_BRANCH:-master} # common variables -POD_REGION_NAME=${POD_REGION_NAME:-Pod1} +CENTRAL_REGION_NAME=${CENTRAL_REGION_NAME:-CentralRegion} TRICIRCLE_NEUTRON_PORT=${TRICIRCLE_NEUTRON_PORT:-20001} TRICIRCLE_CONF_DIR=${TRICIRCLE_CONF_DIR:-/etc/tricircle} TRICIRCLE_STATE_PATH=${TRICIRCLE_STATE_PATH:-/var/lib/tricircle} @@ -18,24 +18,6 @@ TRICIRCLE_API_HOST=${TRICIRCLE_API_HOST:-$SERVICE_HOST} TRICIRCLE_API_PORT=${TRICIRCLE_API_PORT:-19999} TRICIRCLE_API_PROTOCOL=${TRICIRCLE_API_PROTOCOL:-$SERVICE_PROTOCOL} -# tricircle nova_apigw -TRICIRCLE_NOVA_APIGW=$TRICIRCLE_DIR/cmd/nova_apigw.py -TRICIRCLE_NOVA_APIGW_CONF=$TRICIRCLE_CONF_DIR/nova_apigw.conf - -TRICIRCLE_NOVA_APIGW_LISTEN_ADDRESS=${TRICIRCLE_NOVA_APIGW_LISTEN_ADDRESS:-0.0.0.0} -TRICIRCLE_NOVA_APIGW_HOST=${TRICIRCLE_NOVA_APIGW_HOST:-$SERVICE_HOST} -TRICIRCLE_NOVA_APIGW_PORT=${TRICIRCLE_NOVA_APIGW_PORT:-19998} -TRICIRCLE_NOVA_APIGW_PROTOCOL=${TRICIRCLE_NOVA_APIGW_PROTOCOL:-$SERVICE_PROTOCOL} - -# tricircle cinder_apigw -TRICIRCLE_CINDER_APIGW=$TRICIRCLE_DIR/cmd/cinder_apigw.py -TRICIRCLE_CINDER_APIGW_CONF=$TRICIRCLE_CONF_DIR/cinder_apigw.conf - -TRICIRCLE_CINDER_APIGW_LISTEN_ADDRESS=${TRICIRCLE_CINDER_APIGW_LISTEN_ADDRESS:-0.0.0.0} -TRICIRCLE_CINDER_APIGW_HOST=${TRICIRCLE_CINDER_APIGW_HOST:-$SERVICE_HOST} -TRICIRCLE_CINDER_APIGW_PORT=${TRICIRCLE_CINDER_APIGW_PORT:-19997} -TRICIRCLE_CINDER_APIGW_PROTOCOL=${TRICIRCLE_CINDER_APIGW_PROTOCOL:-$SERVICE_PROTOCOL} - # tricircle xjob TRICIRCLE_XJOB=$TRICIRCLE_DIR/cmd/xjob.py TRICIRCLE_XJOB_CONF=$TRICIRCLE_CONF_DIR/xjob.conf diff --git a/tricircle/common/constants.py b/tricircle/common/constants.py index b9b38b5e..65330d69 100644 --- a/tricircle/common/constants.py +++ b/tricircle/common/constants.py @@ -72,6 +72,7 @@ JS_Fail = 'Fail' SP_EXTRA_ID = '00000000-0000-0000-0000-000000000000' TOP = 'top' POD_NOT_SPECIFIED = 'not_specified_pod' +PROFILE_REGION = 'region' # job type JT_ROUTER = 'router' diff --git a/tricircle/db/api.py b/tricircle/db/api.py index b75e8c1f..e78d8f12 100644 --- a/tricircle/db/api.py +++ b/tricircle/db/api.py @@ -127,6 +127,26 @@ def update_pod_service_configuration(context, config_id, update_dict): context, models.PodServiceConfiguration, config_id, update_dict) +def create_resource_mapping(context, top_id, bottom_id, pod_id, project_id, + resource_type): + try: + context.session.begin() + route = core.create_resource(context, models.ResourceRouting, + {'top_id': top_id, + 'bottom_id': bottom_id, + 'pod_id': pod_id, + 'project_id': project_id, + 'resource_type': resource_type}) + context.session.commit() + return route + except db_exc.DBDuplicateEntry: + # entry has already been created + context.session.rollback() + return None + finally: + context.session.close() + + def get_bottom_mappings_by_top_id(context, top_id, resource_type): """Get resource id and pod name on bottom diff --git a/tricircle/network/plugin.py b/tricircle/network/central_plugin.py similarity index 96% rename from tricircle/network/plugin.py rename to tricircle/network/central_plugin.py index 4efb788a..deb2d491 100644 --- a/tricircle/network/plugin.py +++ b/tricircle/network/central_plugin.py @@ -79,7 +79,10 @@ tricircle_opts = [ cfg.StrOpt('bridge_network_type', default='', help=_('Type of l3 bridge network, this type should be enabled ' - 'in tenant_network_types and is not local type.')) + 'in tenant_network_types and is not local type.')), + cfg.BoolOpt('enable_api_gateway', + default=True, + help=_('Whether the Nova API gateway is enabled')) ] tricircle_opt_group = cfg.OptGroup('tricircle') @@ -335,6 +338,14 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, if network.get(external_net.EXTERNAL): self._create_bottom_external_subnet( context, res, network, res['id']) + if res['enable_dhcp']: + try: + t_ctx = t_context.get_context_from_neutron_context(context) + self.helper.prepare_top_dhcp_port( + t_ctx, context, res['tenant_id'], network['id'], res['id']) + except Exception: + self.delete_subnet(context, res['id']) + raise return res def _delete_pre_created_port(self, t_ctx, q_ctx, port_name): @@ -384,7 +395,26 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, # router interface, we cannot directly update bottom port in this case, # otherwise we will fail when attaching bottom port to bottom router # because its device_id is not empty - return super(TricirclePlugin, self).update_port(context, port_id, port) + res = super(TricirclePlugin, self).update_port(context, port_id, port) + if t_constants.PROFILE_REGION in port['port'].get( + 'binding:profile', {}): + region_name = port['port']['binding:profile'][ + t_constants.PROFILE_REGION] + t_ctx = t_context.get_context_from_neutron_context(context) + pod = db_api.get_pod_by_name(t_ctx, region_name) + entries = [(ip['subnet_id'], + t_constants.RT_SUBNET) for ip in res['fixed_ips']] + entries.append((res['network_id'], t_constants.RT_NETWORK)) + entries.append((res['id'], t_constants.RT_PORT)) + + for resource_id, resource_type in entries: + if db_api.get_bottom_id_by_top_id_pod_name( + t_ctx, resource_id, pod['pod_name'], resource_type): + continue + db_api.create_resource_mapping(t_ctx, resource_id, resource_id, + pod['pod_id'], res['tenant_id'], + resource_type) + return res def delete_port(self, context, port_id, l3_port_check=True): t_ctx = t_context.get_context_from_neutron_context(context) @@ -393,16 +423,20 @@ class TricirclePlugin(db_base_plugin_v2.NeutronDbPluginV2, # ports, we just remove records in top pod and leave deletion of # ports and routing entries in bottom pods to xjob if port.get('device_owner') not in NON_VM_PORT_TYPES: - try: - mappings = db_api.get_bottom_mappings_by_top_id( - t_ctx, port_id, t_constants.RT_PORT) - if mappings: - pod_name = mappings[0][0]['pod_name'] - bottom_port_id = mappings[0][1] - self._get_client(pod_name).delete_ports( - t_ctx, bottom_port_id) - except Exception: - raise + if cfg.CONF.tricircle.enable_api_gateway: + # NOTE(zhiyuan) this is a temporary check, after we remove all + # the networking process from nova api gateway, we can remove + # this option + try: + mappings = db_api.get_bottom_mappings_by_top_id( + t_ctx, port_id, t_constants.RT_PORT) + if mappings: + pod_name = mappings[0][0]['pod_name'] + bottom_port_id = mappings[0][1] + self._get_client(pod_name).delete_ports( + t_ctx, bottom_port_id) + except Exception: + raise with t_ctx.session.begin(): core.delete_resources(t_ctx, models.ResourceRouting, filters=[{'key': 'top_id', diff --git a/tricircle/network/exceptions.py b/tricircle/network/exceptions.py index 11ae2e95..1122b700 100644 --- a/tricircle/network/exceptions.py +++ b/tricircle/network/exceptions.py @@ -28,3 +28,7 @@ class DefaultGroupUpdateNotSupported(exceptions.InvalidInput): class BottomPodOperationFailure(exceptions.NeutronException): message = _('Operation for %(resource)s on bottom pod %(pod_name)s fails') + + +class DhcpPortNotFound(exceptions.NotFound): + message = _('Dhcp port for subnet %(subnet_id)s not found') diff --git a/tricircle/network/helper.py b/tricircle/network/helper.py index 04776587..b466a431 100644 --- a/tricircle/network/helper.py +++ b/tricircle/network/helper.py @@ -472,21 +472,17 @@ class NetworkHelper(object): } return body - def prepare_dhcp_port(self, ctx, project_id, b_pod, t_net_id, t_subnet_id, - b_net_id, b_subnet_id): - """Create top dhcp port and map it to bottom dhcp port + def prepare_top_dhcp_port(self, t_ctx, q_ctx, project_id, t_net_id, + t_subnet_id): + """Create top dhcp port - :param ctx: tricircle context + :param t_ctx: tricircle context + :param q_ctx: neutron context :param project_id: project id - :param b_pod: dict of bottom pod :param t_net_id: top network id :param t_subnet_id: top subnet id - :param b_net_id: bottom network id - :param b_subnet_id: bottom subnet id - :return: None + :return: top dhcp port id """ - t_client = self._get_client() - t_dhcp_name = t_constants.dhcp_port_name % t_subnet_id t_dhcp_port_body = { 'port': { @@ -509,8 +505,26 @@ class NetworkHelper(object): # the same IP, each dnsmasq daemon only takes care of VM IPs in # its own pod, VM will not receive incorrect dhcp response _, t_dhcp_port_id = self.prepare_top_element( - ctx, None, project_id, db_api.get_top_pod(ctx), + t_ctx, q_ctx, project_id, db_api.get_top_pod(t_ctx), {'id': t_dhcp_name}, t_constants.RT_PORT, t_dhcp_port_body) + return t_dhcp_port_id + + def prepare_dhcp_port(self, ctx, project_id, b_pod, t_net_id, t_subnet_id, + b_net_id, b_subnet_id): + """Create top dhcp port and map it to bottom dhcp port + + :param ctx: tricircle context + :param project_id: project id + :param b_pod: dict of bottom pod + :param t_net_id: top network id + :param t_subnet_id: top subnet id + :param b_net_id: bottom network id + :param b_subnet_id: bottom subnet id + :return: None + """ + t_dhcp_port_id = self.prepare_top_dhcp_port(ctx, None, project_id, + t_net_id, t_subnet_id) + t_client = self._get_client() t_dhcp_port = t_client.get_ports(ctx, t_dhcp_port_id) dhcp_port_body = self._get_create_dhcp_port_body( project_id, t_dhcp_port, b_subnet_id, b_net_id) diff --git a/tricircle/network/local_plugin.py b/tricircle/network/local_plugin.py new file mode 100644 index 00000000..1fc3ca20 --- /dev/null +++ b/tricircle/network/local_plugin.py @@ -0,0 +1,363 @@ +# Copyright 2015 Huawei Technologies Co., Ltd. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from oslo_config import cfg +from oslo_log import log + +import neutron_lib.constants as q_constants +import neutron_lib.exceptions as q_exceptions + +from neutron.common import utils +from neutron.plugins.ml2 import plugin + +from tricircle.common import client # noqa +import tricircle.common.constants as t_constants +import tricircle.common.context as t_context +from tricircle.common.i18n import _ +from tricircle.common import resource_handle +import tricircle.network.exceptions as t_exceptions +from tricircle.network import helper + + +tricircle_opts = [ + cfg.StrOpt('real_core_plugin', help=_('The core plugin the Tricircle ' + 'local plugin will invoke.')), + cfg.StrOpt('central_neutron_url', help=_('Central Neutron server url'))] + +tricircle_opt_group = cfg.OptGroup('tricircle') +cfg.CONF.register_group(tricircle_opt_group) +cfg.CONF.register_opts(tricircle_opts, group=tricircle_opt_group) + + +LOG = log.getLogger(__name__) + + +class TricirclePlugin(plugin.Ml2Plugin): + def __init__(self): + super(TricirclePlugin, self).__init__() + core_plugins_namespace = 'neutron.core_plugins' + plugin_provider = cfg.CONF.tricircle.real_core_plugin + plugin_class = utils.load_class_by_alias_or_classname( + core_plugins_namespace, plugin_provider) + self.core_plugin = plugin_class() + self.neutron_handle = resource_handle.NeutronResourceHandle( + cfg.CONF.client.auth_url) + self.neutron_handle.endpoint_url = \ + cfg.CONF.tricircle.central_neutron_url + + @staticmethod + def _adapt_network_body(network): + network_type = network.get('provider:network_type') + if network_type == t_constants.NT_LOCAL: + for key in ['provider:network_type', 'provider:physical_network', + 'provider:segmentation_id']: + network.pop(key, None) + elif network_type == t_constants.NT_SHARED_VLAN: + network['provider:network_type'] = 'vlan' + + @staticmethod + def _adapt_port_body_for_client(port): + port.pop('port_security_enabled', None) + port.pop('allowed_address_pairs', None) + remove_keys = [] + for key, value in six.iteritems(port): + if value is q_constants.ATTR_NOT_SPECIFIED: + remove_keys.append(key) + for key in remove_keys: + port.pop(key) + + @staticmethod + def _adapt_port_body_for_call(port): + if 'mac_address' not in port: + port['mac_address'] = q_constants.ATTR_NOT_SPECIFIED + if 'fixed_ips' not in port: + port['fixed_ips'] = q_constants.ATTR_NOT_SPECIFIED + + @staticmethod + def _construct_params(filters, sorts, limit, marker, page_reverse): + params = {} + for key, value in six.iteritems(filters): + params[key] = value + if sorts: + params['sort_key'] = [s[0] for s in sorts] + if page_reverse: + params['sort_dir'] = ['desc' if s[1] else 'asc' for s in sorts] + else: + params['sort_dir'] = ['asc' if s[1] else 'desc' for s in sorts] + if limit: + params['limit'] = limit + if marker: + params['marker'] = marker + return params + + def _ensure_network_subnet(self, context, port): + network_id = port['network_id'] + # get_network will create bottom network if it doesn't exist, also + # create bottom subnets if they don't exist + self.get_network(context, network_id) + + def _ensure_subnet(self, context, network, is_top=True): + subnet_ids = network.get('subnets', []) + if not is_top: + if subnet_ids: + return subnet_ids + else: + t_ctx = t_context.get_context_from_neutron_context(context) + t_network = self.neutron_handle.handle_get( + t_ctx, 'network', network['id']) + return self._ensure_subnet(context, t_network) + if not subnet_ids: + return [] + if len(subnet_ids) == 1: + self.get_subnet(context, subnet_ids[0]) + else: + self.get_subnets(context, filters={'id': subnet_ids}) + return subnet_ids + + def _ensure_subnet_dhcp_port(self, t_ctx, q_ctx, b_subnet): + b_dhcp_ports = self.core_plugin.get_ports( + q_ctx, filters={'network_id': [b_subnet['network_id']], + 'device_owner': ['network:dhcp']}) + if b_dhcp_ports: + return + raw_client = self.neutron_handle._get_client(t_ctx) + params = {'name': t_constants.dhcp_port_name % b_subnet['id']} + t_ports = raw_client.list_ports(**params)['ports'] + if not t_ports: + raise t_exceptions.DhcpPortNotFound(subnet_id=b_subnet['id']) + + dhcp_port_body = \ + helper.NetworkHelper._get_create_dhcp_port_body( + b_subnet['tenant_id'], t_ports[0], b_subnet['id'], + b_subnet['network_id']) + dhcp_port_body['port']['id'] = t_ports[0]['id'] + self.core_plugin.create_port(q_ctx, dhcp_port_body) + + def get_network(self, context, _id, fields=None): + try: + b_network = self.core_plugin.get_network(context, _id, fields) + subnet_ids = self._ensure_subnet(context, b_network, False) + except q_exceptions.NotFound: + t_ctx = t_context.get_context_from_neutron_context(context) + t_network = self.neutron_handle.handle_get(t_ctx, 'network', _id) + if not t_network: + raise q_exceptions.NetworkNotFound(net_id=_id) + self._adapt_network_body(t_network) + b_network = self.core_plugin.create_network(context, + {'network': t_network}) + subnet_ids = self._ensure_subnet(context, t_network) + if subnet_ids: + b_network['subnets'] = subnet_ids + return self._fields(b_network, fields) + + def get_networks(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, page_reverse=False): + # if id is not specified in the filter, we just return network data in + # local Neutron server, otherwise id is specified, we need to retrieve + # network data from central Neutron server and create network which + # doesn't exist in local Neutron server. + if not filters or 'id' not in filters: + return self.core_plugin.get_networks( + context, filters, fields, sorts, limit, marker, page_reverse) + + b_networks = self.core_plugin.get_networks( + context, filters, fields, sorts, limit, marker, page_reverse) + for b_network in b_networks: + subnet_ids = self._ensure_subnet(context, b_network, False) + if subnet_ids: + b_network['subnets'] = subnet_ids + + if len(b_networks) == len(filters['id']): + return b_networks + + t_ctx = t_context.get_context_from_neutron_context(context) + raw_client = self.neutron_handle._get_client(t_ctx) + params = self._construct_params(filters, sorts, limit, marker, + page_reverse) + t_networks = raw_client.list_networks(**params)['networks'] + + t_id_set = set([network['id'] for network in t_networks]) + b_id_set = set([network['id'] for network in b_networks]) + missing_id_set = t_id_set - b_id_set + if missing_id_set: + missing_networks = [network for network in t_networks if ( + network['id'] in missing_id_set)] + for network in missing_networks: + self._adapt_network_body(network) + b_network = self.core_plugin.create_network( + context, {'network': network}) + subnet_ids = self._ensure_subnet(context, network) + if subnet_ids: + b_network['subnets'] = subnet_ids + b_networks.append(self._fields(b_network, fields)) + return b_networks + + def get_subnet(self, context, _id, fields=None): + t_ctx = t_context.get_context_from_neutron_context(context) + try: + b_subnet = self.core_plugin.get_subnet(context, _id, fields) + except q_exceptions.NotFound: + t_subnet = self.neutron_handle.handle_get(t_ctx, 'subnet', _id) + if not t_subnet: + raise q_exceptions.SubnetNotFound(subnet_id=_id) + b_subnet = self.core_plugin.create_subnet(context, + {'subnet': t_subnet}) + if b_subnet['enable_dhcp']: + self._ensure_subnet_dhcp_port(t_ctx, context, b_subnet) + + return self._fields(b_subnet, fields) + + def get_subnets(self, context, filters=None, fields=None, sorts=None, + limit=None, marker=None, page_reverse=False): + # if id is not specified in the filter, we just return subnet data in + # local Neutron server, otherwise id is specified, we need to retrieve + # subnet data from central Neutron server and create subnet which + # doesn't exist in local Neutron server. + if not filters or 'id' not in filters: + return self.core_plugin.get_subnets( + context, filters, fields, sorts, limit, marker, page_reverse) + + t_ctx = t_context.get_context_from_neutron_context(context) + b_subnets = self.core_plugin.get_subnets( + context, filters, fields, sorts, limit, marker, page_reverse) + for b_subnet in b_subnets: + self._ensure_subnet_dhcp_port(t_ctx, context, b_subnet) + if len(b_subnets) == len(filters['id']): + return b_subnets + + raw_client = self.neutron_handle._get_client(t_ctx) + params = self._construct_params(filters, sorts, limit, marker, + page_reverse) + t_subnets = raw_client.list_subnets(**params)['subnets'] + + t_id_set = set([subnet['id'] for subnet in t_subnets]) + b_id_set = set([subnet['id'] for subnet in b_subnets]) + missing_id_set = t_id_set - b_id_set + if missing_id_set: + missing_subnets = [subnet for subnet in t_subnets if ( + subnet['id'] in missing_id_set)] + for subnet in missing_subnets: + b_subnet = self.core_plugin.create_subnet( + context, {'subnet': subnet}) + if b_subnet['enable_dhcp']: + self._ensure_subnet_dhcp_port(t_ctx, context, b_subnet) + b_subnets.append(self._fields(b_subnet, fields)) + return b_subnets + + def create_port(self, context, port): + port_body = port['port'] + network_id = port_body['network_id'] + # get_network will create bottom network if it doesn't exist + self.get_network(context, network_id) + + t_ctx = t_context.get_context_from_neutron_context(context) + raw_client = self.neutron_handle._get_client(t_ctx) + + if port_body['fixed_ips'] is not q_constants.ATTR_NOT_SPECIFIED: + fixed_ip = port_body['fixed_ips'][0] + ip_address = fixed_ip.get('ip_address') + if not ip_address: + # dhcp agent may request to create a dhcp port without + # specifying ip address, we just raise an exception to reject + # this request + raise q_exceptions.InvalidIpForNetwork(ip_address='None') + params = {'fixed_ips': 'ip_address=%s' % ip_address} + t_ports = raw_client.list_ports(**params)['ports'] + if not t_ports: + raise q_exceptions.InvalidIpForNetwork( + ip_address=fixed_ip['ip_address']) + t_port = t_ports[0] + else: + self._adapt_port_body_for_client(port['port']) + t_port = raw_client.create_port(port)['port'] + subnet_id = t_port['fixed_ips'][0]['subnet_id'] + # get_subnet will create bottom subnet if it doesn't exist + self.get_subnet(context, subnet_id) + b_port = self.core_plugin.create_port(context, {'port': t_port}) + return b_port + + def update_port(self, context, _id, port): + if port['port'].get('device_owner', '').startswith('compute') and ( + port['port'].get('binding:host_id')): + # we check both "device_owner" and "binding:host_id" to ensure the + # request comes from nova. and ovs agent will not call update_port. + # it updates port status via rpc and direct db operation + region_name = cfg.CONF.nova.region_name + update_dict = {'binding:profile': { + t_constants.PROFILE_REGION: region_name}} + t_ctx = t_context.get_context_from_neutron_context(context) + self.neutron_handle.handle_update(t_ctx, 'port', _id, + {'port': update_dict}) + return self.core_plugin.update_port(context, _id, port) + + def get_port(self, context, _id, fields=None): + try: + b_port = self.core_plugin.get_port(context, _id, fields) + except q_exceptions.NotFound: + t_ctx = t_context.get_context_from_neutron_context(context) + t_port = self.neutron_handle.handle_get(t_ctx, 'port', _id) + if not t_port: + raise q_exceptions.PortNotFound(port_id=_id) + self._ensure_network_subnet(context, t_port) + self._adapt_port_body_for_call(t_port) + b_port = self.core_plugin.create_port(context, {'port': t_port}) + return self._fields(b_port, fields) + + def get_ports(self, context, filters=None, fields=None, sorts=None, + limit=None, marker=None, page_reverse=False): + # if id is not specified in the filter, we just return port data in + # local Neutron server, otherwise id is specified, we need to retrieve + # port data from central Neutron server and create port which doesn't + # exist in local Neutron server. + if not filters or 'id' not in filters: + return self.core_plugin.get_ports(context, filters, fields, sorts, + limit, marker, page_reverse) + + b_ports = self.core_plugin.get_ports(context, filters, fields, sorts, + limit, marker, page_reverse) + if len(b_ports) == len(filters['id']): + return b_ports + + id_set = set(filters['id']) + b_id_set = set([port['id'] for port in b_ports]) + missing_id_set = id_set - b_id_set + t_ctx = t_context.get_context_from_neutron_context(context) + raw_client = self.neutron_handle._get_client(t_ctx) + t_ports = [] + for port_id in missing_id_set: + # use list_port will cause infinite API call since central Neutron + # server will also use list_port to retrieve port information from + # local Neutron server, so we show_port one by one + try: + t_port = raw_client.show_port(port_id)['port'] + t_ports.append(t_port) + except Exception: + # user passes a nonexistent port id + pass + + for port in t_ports: + self._ensure_network_subnet(context, port) + self._adapt_port_body_for_call(port) + b_port = self.core_plugin.create_port(context, + {'port': port}) + b_ports.append(self._fields(b_port, fields)) + return b_ports + + def delete_port(self, context, _id, l3_port_check=True): + t_ctx = t_context.get_context_from_neutron_context(context) + self.neutron_handle.handle_delete(t_ctx, t_constants.RT_PORT, _id) + self.core_plugin.delete_port(context, _id, l3_port_check) diff --git a/tricircle/tempestplugin/post_test_hook.sh b/tricircle/tempestplugin/post_test_hook.sh index fd45dc4d..f1f9126e 100755 --- a/tricircle/tempestplugin/post_test_hook.sh +++ b/tricircle/tempestplugin/post_test_hook.sh @@ -43,7 +43,7 @@ curl -X POST http://127.0.0.1:19999/v1.0/pods \ # # the following command is to create a flavor wih name='test', # id=1, ram=1024MB, disk=10GB, vcpu=1 -nova flavor-create test 1 1024 10 1 +# nova flavor-create test 1 1024 10 1 image_id=$(openstack image list | awk 'NR==4 {print $2}') # preparation for the tests @@ -80,12 +80,12 @@ iniset $TEMPEST_CONF volume-feature-enabled api_v1 false iniset $TEMPEST_CONF validation connect_method fixed # Run the Compute Tempest tests -cd $TRICIRCLE_TEMPEST_PLUGIN_DIR -sudo BASE=$BASE ./tempest_compute.sh +# cd $TRICIRCLE_TEMPEST_PLUGIN_DIR +# sudo BASE=$BASE ./tempest_compute.sh # Run the Volume Tempest tests -cd $TRICIRCLE_TEMPEST_PLUGIN_DIR -sudo BASE=$BASE ./tempest_volume.sh +# cd $TRICIRCLE_TEMPEST_PLUGIN_DIR +# sudo BASE=$BASE ./tempest_volume.sh # Run the Network Tempest tests cd $TRICIRCLE_TEMPEST_PLUGIN_DIR diff --git a/tricircle/tests/unit/network/test_plugin.py b/tricircle/tests/unit/network/test_central_plugin.py similarity index 99% rename from tricircle/tests/unit/network/test_plugin.py rename to tricircle/tests/unit/network/test_central_plugin.py index 1837c61b..2ddedbf2 100644 --- a/tricircle/tests/unit/network/test_plugin.py +++ b/tricircle/tests/unit/network/test_central_plugin.py @@ -50,11 +50,11 @@ from tricircle.common import exceptions import tricircle.db.api as db_api from tricircle.db import core from tricircle.db import models +import tricircle.network.central_plugin as plugin from tricircle.network.drivers import type_local from tricircle.network.drivers import type_shared_vlan from tricircle.network import helper from tricircle.network import managers -from tricircle.network import plugin from tricircle.tests.unit.network import test_security_groups from tricircle.xjob import xmanager @@ -1020,7 +1020,8 @@ class PluginTest(unittest.TestCase, core.initialize() core.ModelBase.metadata.create_all(core.get_engine()) cfg.CONF.register_opts(q_config.core_opts) - plugin_path = 'tricircle.tests.unit.network.test_plugin.FakePlugin' + plugin_path = \ + 'tricircle.tests.unit.network.test_central_plugin.FakePlugin' cfg.CONF.set_override('core_plugin', plugin_path) self.context = context.Context() self.save_method = manager.NeutronManager._get_default_service_plugins diff --git a/tricircle/tests/unit/network/test_local_plugin.py b/tricircle/tests/unit/network/test_local_plugin.py new file mode 100644 index 00000000..4bfe10fd --- /dev/null +++ b/tricircle/tests/unit/network/test_local_plugin.py @@ -0,0 +1,334 @@ +# Copyright 2015 Huawei Technologies Co., Ltd. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import mock +from mock import patch +import six +import unittest + +from oslo_utils import uuidutils + +import neutron_lib.constants as q_constants +import neutron_lib.exceptions as q_exceptions + +from tricircle.common import constants +import tricircle.common.context as t_context +import tricircle.network.local_plugin as plugin + + +TOP_NETS = [] +TOP_SUBNETS = [] +TOP_PORTS = [] +BOTTOM_NETS = [] +BOTTOM_SUBNETS = [] +BOTTOM_PORTS = [] +RES_LIST = [TOP_NETS, TOP_SUBNETS, TOP_PORTS, + BOTTOM_NETS, BOTTOM_SUBNETS, BOTTOM_PORTS] +RES_MAP = {'network': {True: TOP_NETS, False: BOTTOM_NETS}, + 'subnet': {True: TOP_SUBNETS, False: BOTTOM_SUBNETS}, + 'port': {True: TOP_PORTS, False: BOTTOM_PORTS}} + + +def create_resource(_type, is_top, body): + RES_MAP[_type][is_top].append(body) + + +def update_resource(_type, is_top, resource_id, body): + for resource in RES_MAP[_type][is_top]: + if resource['id'] == resource_id: + resource.update(body) + return copy.deepcopy(resource) + raise q_exceptions.NotFound() + + +def get_resource(_type, is_top, resource_id): + for resource in RES_MAP[_type][is_top]: + if resource['id'] == resource_id: + return copy.deepcopy(resource) + raise q_exceptions.NotFound() + + +def list_resource(_type, is_top, filters=None): + if not filters: + return [copy.deepcopy(resource) for resource in RES_MAP[_type][is_top]] + ret = [] + for resource in RES_MAP[_type][is_top]: + pick = True + for key, value in six.iteritems(filters): + if resource.get(key) not in value: + pick = False + break + if pick: + ret.append(copy.deepcopy(resource)) + return ret + + +def delete_resource(_type, is_top, body): + RES_MAP[_type][is_top].append(body) + + +class FakeCorePlugin(object): + def create_network(self, context, network): + create_resource('network', False, network['network']) + return network['network'] + + def get_network(self, context, _id, fields=None): + return get_resource('network', False, _id) + + def get_networks(self, context, filters=None, fields=None, sorts=None, + limit=None, marker=None, page_reverse=False): + return list_resource('network', False, filters) + + def create_subnet(self, context, subnet): + create_resource('subnet', False, subnet['subnet']) + return subnet['subnet'] + + def update_subnet(self, context, _id, subnet): + return update_resource('subnet', False, _id, subnet['subnet']) + + def get_subnet(self, context, _id, fields=None): + return get_resource('subnet', False, _id) + + def create_port(self, context, port): + create_resource('port', False, port['port']) + return port['port'] + + def get_port(self, context, _id, fields=None): + return get_resource('port', False, _id) + + def get_ports(self, context, filters=None, fields=None, sorts=None, + limit=None, marker=None, page_reverse=False): + return list_resource('port', False, filters) + + +class FakeSession(object): + class WithWrapper(object): + def __enter__(self): + pass + + def __exit__(self, type, value, traceback): + pass + + def begin(self, subtransactions=True): + return FakeSession.WithWrapper() + + +class FakeContext(object): + def __init__(self): + self.session = FakeSession() + + +class FakeClient(object): + def list_networks(self, **kwargs): + return {'networks': list_resource('network', True, kwargs)} + + def create_port(self, port): + if 'id' not in port['port']: + port['port']['id'] = uuidutils.generate_uuid() + if 'fixed_ips' not in port['port']: + for subnet in TOP_SUBNETS: + if subnet['network_id'] == port['port']['network_id']: + ip = {'subnet_id': subnet['id'], + 'ip_address': subnet['cidr'][:-4] + '3'} + port['port']['fixed_ips'] = [ip] + create_resource('port', True, port['port']) + return port + + def show_port(self, port_id): + return {'port': get_resource('port', True, port_id)} + + def list_ports(self, **kwargs): + def find_ip_address(port, ip_address): + for ip in port.get('fixed_ips', []): + if ip['ip_address'] == ip_address: + return True + return False + + ports = [] + for port in TOP_PORTS: + pick = True + for key, value in six.iteritems(kwargs): + if key == 'fixed_ips': + if not find_ip_address(port, value.split('=')[1]): + pick = False + break + elif port.get(key) != value: + pick = False + break + if pick: + ports.append(copy.deepcopy(port)) + return {'ports': ports} + + +class FakeNeutronHandle(object): + def _get_client(self, context): + return FakeClient() + + def handle_get(self, context, _type, _id): + return get_resource(_type, True, _id) + + +class FakePlugin(plugin.TricirclePlugin): + def __init__(self): + self.core_plugin = FakeCorePlugin() + self.neutron_handle = FakeNeutronHandle() + + +class PluginTest(unittest.TestCase): + def setUp(self): + self.tenant_id = uuidutils.generate_uuid() + self.plugin = FakePlugin() + self.context = FakeContext() + + def _prepare_resource(self): + network_id = uuidutils.generate_uuid() + subnet_id = uuidutils.generate_uuid() + port_id = uuidutils.generate_uuid() + t_net = {'id': network_id, + 'tenant_id': self.tenant_id, + 'name': 'net1', + 'provider:network_type': constants.NT_SHARED_VLAN, + 'subnets': [subnet_id]} + t_subnet = {'id': subnet_id, + 'tenant_id': self.tenant_id, + 'name': 'subnet1', + 'network_id': network_id, + 'cidr': '10.0.1.0/24', + 'enable_dhcp': True} + t_port = {'id': port_id, + 'tenant_id': self.tenant_id, + 'admin_state_up': True, + 'name': constants.dhcp_port_name % subnet_id, + 'network_id': network_id, + 'mac_address': 'fa:16:3e:96:41:02', + 'device_owner': 'network:dhcp', + 'device_id': 'reserved_dhcp_port', + 'fixed_ips': [{'subnet_id': subnet_id, + 'ip_address': '10.0.1.2'}], + 'binding:profile': {}} + TOP_NETS.append(t_net) + TOP_SUBNETS.append(t_subnet) + TOP_PORTS.append(t_port) + return t_net, t_subnet, t_port + + def _validate(self, net, subnet, port): + b_net = self.plugin.get_network(self.context, net['id']) + net.pop('provider:network_type') + b_net_type = b_net.pop('provider:network_type') + b_subnet = get_resource('subnet', False, subnet['id']) + b_port = get_resource('port', False, port['id']) + b_net.pop('project_id') + b_subnet.pop('project_id') + port.pop('name') + b_port.pop('name') + self.assertDictEqual(net, b_net) + self.assertDictEqual(subnet, b_subnet) + self.assertEqual('vlan', b_net_type) + self.assertDictEqual(port, b_port) + + @patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock) + def test_get_network(self): + t_net, t_subnet, t_port = self._prepare_resource() + self._validate(t_net, t_subnet, t_port) + + @patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock) + def test_get_networks(self): + t_net1, t_subnet1, t_port1 = self._prepare_resource() + t_net2, t_subnet2, t_port2 = self._prepare_resource() + self.plugin.get_networks(self.context, + {'id': [t_net1['id'], t_net2['id'], + 'fake_net_id']}) + self._validate(t_net1, t_subnet1, t_port1) + self._validate(t_net2, t_subnet2, t_port2) + + @patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock) + def test_create_port(self): + t_net, t_subnet, t_port = self._prepare_resource() + port = { + 'port': {'network_id': t_net['id'], + 'fixed_ips': q_constants.ATTR_NOT_SPECIFIED} + } + t_port = self.plugin.create_port(self.context, port) + b_port = get_resource('port', False, t_port['id']) + self.assertDictEqual(t_port, b_port) + + @patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock) + def test_create_port_ip_specified(self): + t_net, t_subnet, t_port = self._prepare_resource() + port_body = { + 'port': {'network_id': t_net['id'], + 'fixed_ips': [{'ip_address': '10.0.1.4'}]} + } + self.assertRaises(q_exceptions.InvalidIpForNetwork, + self.plugin.create_port, self.context, port_body) + + port_id = uuidutils.generate_uuid() + t_port = {'id': port_id, + 'tenant_id': self.tenant_id, + 'admin_state_up': True, + 'network_id': t_net['id'], + 'mac_address': 'fa:16:3e:96:41:04', + 'fixed_ips': [{'subnet_id': t_subnet['id'], + 'ip_address': '10.0.1.4'}], + 'binding:profile': {}} + TOP_PORTS.append(t_port) + b_port = self.plugin.create_port(self.context, port_body) + self.assertDictEqual(t_port, b_port) + + @patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock) + def test_get_port(self): + t_net, t_subnet, t_port = self._prepare_resource() + port_id = uuidutils.generate_uuid() + t_port = {'id': port_id, + 'tenant_id': self.tenant_id, + 'admin_state_up': True, + 'network_id': t_net['id'], + 'mac_address': 'fa:16:3e:96:41:04', + 'fixed_ips': [{'subnet_id': t_subnet['id'], + 'ip_address': '10.0.1.4'}], + 'binding:profile': {}} + TOP_PORTS.append(t_port) + t_port = self.plugin.get_port(self.context, port_id) + b_port = get_resource('port', False, t_port['id']) + self.assertDictEqual(t_port, b_port) + + @patch.object(t_context, 'get_context_from_neutron_context', new=mock.Mock) + def test_get_ports(self): + t_net, t_subnet, t_port = self._prepare_resource() + t_ports = [] + for i in (4, 5): + port_id = uuidutils.generate_uuid() + t_port = {'id': port_id, + 'tenant_id': self.tenant_id, + 'admin_state_up': True, + 'network_id': t_net['id'], + 'mac_address': 'fa:16:3e:96:41:04', + 'fixed_ips': [{'subnet_id': t_subnet['id'], + 'ip_address': '10.0.1.%d' % i}], + 'binding:profile': {}} + TOP_PORTS.append(t_port) + t_ports.append(t_port) + self.plugin.get_ports(self.context, + {'id': [t_ports[0]['id'], t_ports[1]['id'], + 'fake_port_id']}) + for i in (0, 1): + b_port = get_resource('port', False, t_ports[i]['id']) + b_port.pop('project_id') + self.assertDictEqual(t_ports[i], b_port) + + def tearDown(self): + for res in RES_LIST: + del res[:]