跳到正文 Lab 05: Introduction to RESTful Services (Flask, FastAPI) · Jimmy Huang's Blog
← Week 5 Web Service and Service-Oriented Architecture 学习 › C6001 › 2026 S1 › FIT5225 Cloud computing and security › Week 5 Web Service and Service-Oriented Architecture Lab 05: Introduction to RESTful Services (Flask, FastAPI) Lab 05: Introduction to RESTful Services (Flask, FastAPI) 本页目录· 56 条 这一页有部分内容暂时无法显示,其余内容完整。
Lab学后总结 › 此处有一段内容暂不支持显示。
此处有一段内容暂不支持显示。
此处有一段内容暂不支持显示。
此处有一段内容暂不支持显示。
Overview 本周的主题是 RESTful Services 入门 。这周不是只让你“把程序跑起来”,而是让你把 RESTful Web Service 的一整条主线走一遍:
先当 API 的调用者 :用 OpenStack 的 API / SDK 去程序化创建云资源
再当 API 的提供者 :用 Python + FastAPI 写一个最基本的 Todo Web Service
最后当 API 的测试者 :用 curl 验证 CRUD,用 Locust 做负载测试
所以这周真正要掌握的,不是几条孤立命令,而是下面这条流程:
另外,这周有一个很重要的切换点:
Task 0 里的 VM 在你自己的 pt-xxxx 项目里,它是这周的“客户端 / 工作台”
Task 1 里通过 API 创建出来的新 VM 在 FIT5225-Cloud shared project 里
Task 2 ~ Task 4 默认使用这个新创建出来的 VM 继续完成
Goal 这周实验希望你掌握下面几件事:
本页目录 会配置 OpenStack API 访问环境,并用 Python SDK 程序化创建资源
会用 FastAPI 跑一个最简单的 Todo Web Service
会用 curl 和自动生成的 /docs 页面测试 API
会用 Locust 做基础压测,并能说出如何提升并发能力和 RPS
Task 0: Create a VM in your pt-xxxx Nectar project 在你自己的 pt-xxxx 项目里创建一台 Ubuntu VM,作为后面调用 OpenStack API 的客户端
这台机器不是本周最后部署 Todo 服务的重点机器,而是前半段实验的工作台
配置要满足 Lab 要求:Ubuntu 24.04、2 Core、4GB RAM
核心检查点:VM 能成功创建、拿到 IP、你能 SSH 登录
Step 0.1:在个人项目里创建客户端 VM 在 Nectar Dashboard 中进入你自己的 pt-xxxx 项目,创建一台新的实例:
先准备一台统一的 Linux 环境,后面所有 OpenStack API 认证文件和脚本都先放在这台机器上执行。
Lab 明确希望大家在 Ubuntu VM 上完成前半段操作,这样每个人环境更一致,不容易出现本机差异问题。
Step 0.2:SSH 登录并确认机器可用 如果你使用的镜像默认用户名不是 ubuntu,请替换成实际用户名。
可以成功进入远程终端,说明这台客户端 VM 已经可用。
Task 1: Using OpenStack RESTful API to Create Resources 下载 OpenStack API 所需的配置文件,并重置 API password
把文件传到客户端 VM,安装 Python 与 openstacksdk
配置 clouds.yaml 与 OpenRC,让当前 shell 拥有 OpenStack 认证信息
修改并运行 openstack-api.py,程序化创建 key pair 和新 VM
核心检查点: echo $OS_USERNAME 有值、脚本跑通、共享项目里能看到新 VM、能 SSH 进去
Step 1.1:下载配置文件并重置 OpenStack API password 你需要先在 Nectar Dashboard 完成下面三件事:
下载 OpenStack API 配置文件 进入 API Access
下载 clouds.yaml
下载 OpenStack RC file
重置 OpenStack API password 下载老师提供的脚本 下载此内容需要加入共享Project: FIT5225-Cloud,如果没被教学组加入,需要在ED主动联系教学组
把调用 OpenStack API 所需要的“配置 + 认证 + 脚本”先准备齐。
Step 1.2:把文件上传到客户端 VM 最直接的做法是从你本地电脑把三个文件传到 Task 0 创建的客户端 VM。
如果你的下载目录、文件名、用户名或 IP 不同,请替换成自己的实际值。
Step 1.3:安装 Python、pip、venv 和 OpenStack SDK
使用 venv继续安装 安装运行 openstack-api.py 所需的 Python 环境和 SDK。
你后面不是自己手搓 HTTP 请求,而是通过 openstacksdk 这个 Python 库去调用 OpenStack API。
openstacksdk 可以理解成“Python 程序和 OpenStack REST API 之间的桥”。
你写的是 Python 代码,但背后本质上还是在调用云平台的 API。
Step 1.4:放置 clouds.yaml 并导入 OpenRC 配置 先把 clouds.yaml 放到 OpenStack 默认会查找的位置:
然后把 OpenRC 文件导入到当前 shell:
系统会提示你输入刚才重置的 OpenStack API password。输入后回车。
让当前终端拿到 OpenStack 认证环境变量,同时让 SDK 能找到默认的 cloud 配置。
如果这一步没成功,后面的脚本几乎一定无法认证成功。
echo $OS_USERNAME 应该输出你的 Nectar 用户名,而不是空白。
Step 1.5:修改 openstack-api.py
Step 1.6:运行脚本并验证资源是否真的创建成功 正式通过 Python SDK 调 OpenStack API,去创建新的 key pair 和新的 VM。
如果程序没有报错,把最后一行日志直接复制回终端执行。
Task 2: Creating a Simple Todo Application
Step 2.1:把 todo-app.py 传到新创建的 VM
推荐做法:两跳传输 因为 Task 1 新 VM 的私钥是脚本在 客户端 VM 上生成的,所以最稳的流程是:
先把 todo-app.py 传到 客户端 VM
再从 客户端 VM 传到 新 VM
先从本地传到 客户端 VM
再从 客户端 VM 传到 新 VM
Step 2.2:安装应用依赖 把 FastAPI 服务运行起来所需要的依赖装齐,同时把 curl、 jq 这些后面测试 API 的工具也一起装好。
Step 2.3:先补上缺失的 DELETE 接口 Lab 特别说明了:提供的 Todo 应用里 删除任务的服务缺失 ,这是要你补上的练习。
请打开 todo-app.py,把下面末尾app.delete修改成如下所示:
因为后面 Task 3 明确要求你测试 DELETE。如果你不先把这个路由补进去,后面 DELETE 请求要么 404,要么 405。
Task 3: Testing Web Services 启动 FastAPI Todo 服务,并让它监听外部可访问地址
通过 /docs 页面和 curl 做 CRUD 测试
观察为什么第一次 POST 会得到 400 Bad Request
理解 Content-Type: application/json 和资源路径 /tasks/{id} 的作用
核心检查点: /docs 能访问、GET / POST / PUT / DELETE 都有明确结果
Step 3.1:启动 FastAPI 服务 启动 FastAPI 自带的开发服务器,并让它监听所有网卡地址。
0.0.0.0 的意思不是某个固定 IP,而是“接受这台机器所有网络接口上的请求”。
接下来请 新开一个 SSH 终端 来做 curl 测试。
Step 3.2:开放 8000 端口并访问自动生成的 API 文档 先在云平台安全规则里开放新 VM 的 TCP 8000 入站访问(默认自动开启无需修改)
利用 FastAPI 自动生成的交互式文档页面,快速查看现有接口并做简单测试。
你应该能看到 Swagger 风格的 API 页面,并且里面列出了 /todo/api/v1.0/tasks 等接口。
Step 3.3:先获取当前任务列表(GET) 用 HTTP GET 请求读取当前所有 Todo 项。
Step 3.4:按 Lab 原样尝试创建新任务,先观察 400 错误(POST) 先执行这条“故意不带 Content-Type”的命令:
复现 Lab 里提到的 400 Bad Request,让你看到“请求体长得像 JSON”和“服务端知道这是 JSON”不是一回事。
因为你虽然传了 JSON 字符串,但没有明确告诉服务端:
很多 REST API 问题,不是接口本身坏了,而是 请求方法、认证、请求头、请求体格式 没写对。
Step 3.5:加上 Content-Type,重新创建任务(POST) 这次应该能成功创建一个新任务,并返回包含任务内容的 JSON。
用标准 JSON 请求体调用 POST 接口,真正创建一个新的 Todo 项。
Step 3.6:更新刚才创建的任务(PUT) /tasks 表示任务集合
/tasks/2 表示 id=3 的那个具体任务
Step 3.7:获取单个任务(GET one task)
Step 3.8:删除单个任务(DELETE) 调用在 Step 2.3 补上的 DELETE 路由,删除指定 Todo 项。
Task 4: Web Services Load Testing with Locust 让你的 Todo 服务可以从公网访问
在你自己的电脑上创建 locustfile.py
启动 Locust Web UI,对 Todo API 发压测请求
逐步提高并发,观察什么时候出现失败、超时或 RPS 上不去
核心检查点:服务公网可访问、Locust 能跑起来、你能记录并发用户数 / RPS / 失败率
Step 4.1:先确认你的 Todo 服务能从公网访问 先确认新 VM 的 8000 端口已经开放,然后在你本地电脑上测试公网访问。
浏览器方式
命令行方式(更稳) 确认你的服务已经不是“只有 VM 自己能访问”,而是能从外部网络打到。
Locust 是在你自己的电脑上发请求,如果服务根本没有公网可达性,后面所有压测都只会报连接错误。
Step 4.2:在你本地电脑创建 locustfile.py在你本机新建 locustfile.py,内容如下:
如果你的初始任务里没有 id=1,也要把 /tasks/1 改成一个实际存在的任务 ID。
Step 4.3:如果本机还没装 Locust,先安装 如果你之前的实验已经安装过 Locust,可以跳过。
Step 4.4:启动 Locust 并打开 Web UI Locust 会根据 locustfile.py 的定义,持续向你的 Todo API 发请求,并统计 RPS、响应时间和失败率。
Step 4.5:逐步提高并发并记录结果 Users:当前并发用户数
RPS:每秒请求数
Failures:失败请求数
Response Time:响应时间
是否开始出现超时、连接失败、5xx 错误
80 user
100 user
150 user
200 user
Step 4.6:Homework / Reflection
问题 1:How many concurrent users / RPS can your application handle?
问题 2:How can we handle more concurrent users and improve RPS?
Summary 会调别人的 API 你不是只会在网页里点按钮,而是学会了通过 OpenStack 的配置、认证和 Python SDK,用代码去创建云资源。
会做自己的 API 你把一个 Todo 应用跑成了一个真正可调用的 RESTful Web Service,理解了 GET / POST / PUT / DELETE 和资源路径之间的关系。
会验证和压测 API 你不只是“看起来跑起来了”,还用 curl 验证了功能,用 Locust 观察了并发和 RPS,开始把“服务能不能扛住负载”也纳入考虑。
本周必须理解的知识点 RESTful API 是围绕 resource + HTTP method 来组织的
GET / POST / PUT / DELETE 基本对应 CRUD 的读取 / 创建 / 更新 / 删除
clouds.yaml 和 OpenRC 文件作用不同,但都和 OpenStack API 访问有关
source <openrc-file> 的本质是把认证环境变量导入当前 shell
FastAPI 会把 HTTP 请求映射到 Python 函数,并自动生成 /docs
Content-Type: application/json 会影响服务端如何解析请求体
0.0.0.0 代表监听所有网卡地址,外部访问时通常必须这样配置
Locust 关注的不只是“能不能访问”,还包括并发用户数、RPS、失败率和响应时间
关键概念对比
clouds.yaml vs OpenRC 更偏向给 SDK / 程序使用,让程序知道要连哪个 cloud、读什么配置。
更偏向给当前 shell 使用,把一组 OS_ 开头的环境变量导入终端。
clouds.yaml:给程序看的
OpenRC:给当前终端看的
localhost vs 0.0.0.0让服务监听所有网卡地址,外部才能通过这台机子的 IP 访问。
localhost:只有自己能看
0.0.0.0:外面也能打进来
GET / POST / PUT / DELETEGET:读取资源
POST:创建新资源
PUT:更新已有资源
DELETE:删除已有资源
手动点 Dashboard vs 程序化调 API 适合重复、可自动化、可写进脚本的操作,也更接近真实云平台运维与开发流程。
实验问题回答
为什么第一次 POST 会得到 400 Bad Request? 因为请求体虽然写成了 JSON 字符串,但没有通过 Content-Type: application/json 明确告诉服务端这是 JSON,所以服务端无法按预期解析请求体。
为什么更新和删除都要写成 /tasks/{id}? 因为 REST 风格里,URL 不只是地址,也是资源标识。
/tasks 指的是任务集合, /tasks/2 指的是具体某一个任务。不写 ID,服务端就不知道你到底要更新或删除哪一个资源。
如果要支撑更多并发和更高 RPS,最先应该做什么? 最先应该把开发环境的 FastAPI server 换成生产用 ASGI 部署方式,并增加 worker;然后把当前这种内存数据结构改成更适合并发访问的持久化方案,再配合扩容、缓存、负载均衡和性能监控一起优化。
老师可能会问什么 什么是 RESTful service?什么是 resource?
GET / POST / PUT / DELETE 分别适合什么场景?
为什么 POST 第一次会报 400?
Content-Type: application/json 到底在告诉服务端什么?
为什么要 source OpenRC 文件?
clouds.yaml 和 OpenRC 文件有什么区别?
openstack-api.py 自动帮你做了哪些事?
为什么启动 FastAPI 时要用 0.0.0.0?
为什么 Locust 要从你的电脑去压测,而不是只在服务器本机测?
如果应用扛不住更高并发,你会先从哪几个方向优化?
客户端 VM
-> 配 OpenStack 认证
-> 调 OpenStack API 创建资源
新创建的 VM
-> 部署 Todo Web Service
-> 用 curl / docs 测 CRUD
-> 用 Locust 做压测http://<your-server-ip>:8000/docsContent-Type: application/jsonhttp://<your-public-ip>:8000/docs本次压测结果显示,该 Todo API 在 100 并发用户下表现最好,RPS 约为 33,响应时间低,只有极少量失败,可视为基本稳定。
在 150 并发时,系统仍可正常运行,RPS 提升到约 49,但失败数开始增加。
在 200 并发时,RPS 约为 66,但失败进一步上升,说明系统已经接近当前配置下的承载上限。
在 300 到 400 并发时,服务虽然仍能运行,但失败更加明显,因此不能视为稳定承载范围。为了处理更多并发用户并提升 RPS,第一步是把 FastAPI 自带的开发服务器替换成适合生产环境的 ASGI 部署方式,例如使用 Uvicorn,或者使用带有多个 worker 的 Gunicorn。
应用程序也不应该只把所有 todo 项存放在内存里,而是应该改用正规的数据库,这样可以让请求处理更加可靠,也更容易扩展。
另外,我们还可以通过使用更大的虚拟机、优化 API 代码、减少不必要的开销,以及对高频请求的数据加入缓存,来进一步提升性能。
同时,持续监控 CPU、内存、延迟和错误率也很重要,这样就能尽早发现瓶颈并及时修复。ssh -i < path-to-your-original-ke y> ubuntu@ < client-vm-public-i p> whoami hostname lsb_release -a scp -i < path-to-your-original-ke y> ~/Downloads/clouds.yaml ubuntu@ < client-vm-public-i p> :~ scp -i < path-to-your-original-ke y> ~/Downloads/FIT5225-Cloud-openrc.sh ubuntu@ < client-vm-public-i p> :~ scp -i < path-to-your-original-ke y> ~/Downloads/openstack-api.py ubuntu@ < client-vm-public-i p> :~ ssh -i < path-to-your-original-ke y> ubuntu@ < client-vm-public-i p> ls sudo apt update sudo apt install -y python3 python3-pip python3-venv mkdir -p ~/venv python3 -m venv ~/venv source ~/venv/bin/activate pip3 install --upgrade pip pip3 install openstacksdk mkdir -p ~/.config/openstack 43965 source ~/FIT5225-Cloud-openrc.sh source ~/venv/bin/activate scp -i < path-to-your-original-ke y> ~/Downloads/todo-app.py ubuntu@ < client-vm-public-i p> :~ scp -i ~/ < generated-private-key-fil e> ~/todo-app.py ubuntu@ < new-vm-i p> :~ ssh -i ~/ < generated-private-key-fil e> ubuntu@ < new-vm-i p> ls sudo apt update sudo apt install -y python3-pip curl jq pip3 install " fastapi[standard] " python3 --version curl --version jq --version @app.delete( ' /todo/api/v1.0/tasks/{task_id} ' , response_model=dict) async def delete_task(task_id: int, username: str = Depends(get_current_username)): for index, todo in enumerate(tasks): if todo.id == task_id: del tasks[index] return { " message " : f"Todo {task_id} has been deleted." } raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail= " Todo is not found " ) python3 -m fastapi dev todo-app.py --host 0.0.0.0 curl -X GET http://localhost:8000/todo/api/v1.0/tasks --basic --user fastapi:FIT5225 | jq curl -X POST --user fastapi:FIT5225 \ -d ' {"title":"new-todo"} ' \ http://localhost:8000/todo/api/v1.0/tasks | jq curl -X POST --user fastapi:FIT5225 \ -d ' {"title":"new-todo"} ' \ --header " Content-Type: application/json " \ http://localhost:8000/todo/api/v1.0/tasks | jq curl -X PUT --user fastapi:FIT5225 \ -d ' {"title":"new-new", "description":"I am new"} ' \ --header " Content-Type: application/json " \ http://localhost:8000/todo/api/v1.0/tasks/3 | jq curl -X GET http://localhost:8000/todo/api/v1.0/tasks/3 \ --basic --user fastapi:FIT5225 | jq curl -X DELETE --user fastapi:FIT5225 \ --header " Content-Type: application/json " \ http://localhost:8000/todo/api/v1.0/tasks/3 | jq curl -X GET http:// < your-public-i p> :8000/todo/api/v1.0/tasks --basic --user fastapi:FIT5225 | jq from locust import HttpUser, task, between from requests.auth import HTTPBasicAuth auth = HTTPBasicAuth( " fastapi " , " FIT5225 " ) class UserBehavior(HttpUser): wait_time = between(1, 5) @task(10) def get_tasks(self): self.client.get( " /todo/api/v1.0/tasks " , auth=auth) @task(5) def get_task(self): self.client.get( " /todo/api/v1.0/tasks/1 " , auth=auth) @task(3) def create_task(self): self.client.post( " /todo/api/v1.0/tasks " , json={ " title " : " New Task " }, auth=auth) @task(2) def update_task(self): self.client.put( " /todo/api/v1.0/tasks/1 " , json={ " title " : " Updated Task " }, auth=auth) python3 -m venv ~/locust-venv source ~/locust-venv/bin/activate pip install --upgrade pip pip install locust import secrets from typing import Annotated, List, Optional from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.responses import PlainTextResponse, JSONResponse from fastapi.exceptions import RequestValidationError from pydantic import BaseModel class TodoItem(BaseModel): id: int title: str | None = None description: str | None = "" done: bool = False app = FastAPI() security = HTTPBasic() # default username is fastapi, password is FIT5225. In practice, you may need other authentication methods def get_current_username( credentials: Annotated[HTTPBasicCredentials, Depends(security)], ): current_username_bytes = credentials.username.encode( " utf8 " ) correct_username_bytes = b " fastapi " is_correct_username = secrets.compare_digest( current_username_bytes, correct_username_bytes ) current_password_bytes = credentials.password.encode( " utf8 " ) correct_password_bytes = b " FIT5225 " is_correct_password = secrets.compare_digest( current_password_bytes, correct_password_bytes ) if not (is_correct_username and is_correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail= " Incorrect username or password " , headers={ " WWW-Authenticate " : " Basic " }, ) return credentials.username # Initialize some sample data in memeory tasks = [ TodoItem(id=1, title= ' Buy groceries ' , description= ' Milk, Cheese, Pizza, Fruit, Tylenol ' , done=False), TodoItem(id=2, title= ' Learn Python ' , description= ' Need to find a good Python tutorial on the web ' , done=False) ] next_id = 3 @app.exception_handler(HTTPException) async def http_exception_handler(request, exc): if exc.status_code == status.HTTP_401_UNAUTHORIZED: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, # For API calls, return 403 instead of 401 to prevent browsers from displaying the default auth dialog content={ " message " : " Unauthorized access " }, headers={ " WWW-Authenticate " : " Basic " }, ) return JSONResponse( status_code=exc.status_code, content={ " message " : exc.detail}, headers=exc.headers, ) @app.exception_handler(RequestValidationError) async def validation_exception_handler(request, exc): return PlainTextResponse(str(exc), status_code=400) # It's a good practice to have a public API endpoint allowing enduser to check status of the API @app.get( ' /api/health ' ) async def public(): return { " message " : " The todo API is alive. " } @app.get( ' /todo/api/v1.0/tasks ' , response_model=List[TodoItem] ) async def get_tasks(username: str = Depends(get_current_username)): return tasks @app.get( ' /todo/api/v1.0/tasks/{task_id} ' , response_model=TodoItem) async def get_task(task_id:int, username: str = Depends(get_current_username)): for todo in tasks: if todo.id == task_id: return todo raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail= " Todo is not found " ) @app.post( ' /todo/api/v1.0/tasks ' , response_model=TodoItem, status_code=status.HTTP_201_CREATED) async def create_task(todo:dict, username: str = Depends(get_current_username)): global next_id todo_item = TodoItem(id=next_id, **todo) tasks.append(todo_item) next_id += 1 return todo_item @app.put( ' /todo/api/v1.0/tasks/{task_id} ' , response_model=TodoItem) async def update_task(task_id:int, updated_todo:dict, username: str = Depends(get_current_username)): for index, todo in enumerate(tasks): if todo.id == task_id: updated_todo = TodoItem(id=task_id, **updated_todo) tasks[index] = updated_todo return updated_todo raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail= " Todo is not found " ) @app.delete( ' /todo/api/v1.0/tasks/{task_id} ' , response_model=dict) async def delete_task(task_id:int, username: str = Depends(get_current_username)): # please implement return { " message " : " Not implemented. " } import openstack import os import errno IMAGE_NAME = " NeCTAR Ubuntu 22.04 LTS (Jammy) amd64 " FLAVOR_NAME = " mon.training.small " KEYPAIR_NAME = " openstacksdk-example " SERVER_NAME = " demo-jay " VM_DEFAULT_USERNAME = " ubuntu " NETWORK_NAME = " Classic Provider " SSH_DIR = ' {home}/.ssh ' .format(home=os.path.expanduser( " ~ " )) PRIVATE_KEYPAIR_FILE = ' {ssh_dir}/id_rsa.{key} ' .format(ssh_dir=SSH_DIR, key=KEYPAIR_NAME) def create_keypair(conn): keypair = conn.compute.find_keypair(KEYPAIR_NAME) if not keypair: print( " Creating Key Pair: " ) keypair = conn.compute.create_keypair(name=KEYPAIR_NAME) print(keypair) try: os.mkdir(SSH_DIR) except OSError as e: if e.errno != errno.EEXIST: raise e with open(PRIVATE_KEYPAIR_FILE, ' w ' ) as f: f.write( " %s " % keypair.private_key) os.chmod(PRIVATE_KEYPAIR_FILE, 0o400) return keypair def attach_security_group(conn, server, sg= " _default " ): print(f " Attaching security group: {sg} to {server.name} " ) # Fetch existing security groups attached to the server existing_sgs = conn.compute.fetch_server_security_groups(server) # Remove the default security group if it exists for existing_sg in existing_sgs: if existing_sg == " default " : print(f " Removing default security group from {server.name} " ) conn.compute.remove_security_group_from_server(server=server, security_group= " default " ) # Add the desired security group for security_group in conn.network.security_groups(): if security_group.name.lower() == sg.lower(): conn.compute.add_security_group_to_server(server=server, security_group=sg) print(f " Security group {sg} attached to {server.name} " ) return print(f " Security group {sg} not found. " ) def create_server(conn): print( " Creating Server {0}: " .format(SERVER_NAME)) image = conn.compute.find_image(IMAGE_NAME) flavor = conn.compute.find_flavor(FLAVOR_NAME) network = conn.network.find_network(NETWORK_NAME) keypair = create_keypair(conn) server = conn.compute.create_server( name=SERVER_NAME, image_id=image.id, flavor_id=flavor.id, networks=[{ "uuid" : network.id}], key_name=keypair.name) server = conn.compute.wait_for_server(server) # **Call attach_security_group here** attach_security_group(conn, server, sg= " _default " ) print( " ssh -i {key} {username}@{ip} " .format( key=PRIVATE_KEYPAIR_FILE, username=VM_DEFAULT_USERNAME, ip=server.access_ipv4)) # Initialize and turn on debug logging openstack.enable_logging(debug=True) # Initialize cloud conn = openstack.connect(cloud= ' envvars ' ) create_server(conn)