Skip to content

Commit 47d9de1

Browse files
committed
feat: Add blazar reservation changes
1 parent 30f918c commit 47d9de1

33 files changed

Lines changed: 2955 additions & 21 deletions

File tree

src/client/blazar/index.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Base from '../client/base';
2+
import { blazarBase } from '../client/constants';
3+
4+
export class BlazarClient extends Base {
5+
get baseUrl() {
6+
return blazarBase();
7+
}
8+
9+
get resources() {
10+
return [
11+
{
12+
key: 'leases',
13+
responseKey: 'lease',
14+
},
15+
{
16+
name: 'hosts',
17+
key: 'os-hosts',
18+
responseKey: 'host',
19+
},
20+
{
21+
key: 'floatingips',
22+
responseKey: 'floatingip',
23+
},
24+
];
25+
}
26+
}
27+
28+
const blazarClient = new BlazarClient();
29+
export default blazarClient;

src/client/client/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const endpointVersionMap = {
3737
magnum: 'v1',
3838
designate: 'v2',
3939
masakari: 'v1',
40+
blazar: 'v1',
4041
};
4142

4243
export const endpointsDefault = {
@@ -79,6 +80,7 @@ export const zunBase = () => getOpenstackEndpoint('zun');
7980
export const magnumBase = () => getOpenstackEndpoint('magnum');
8081
export const designateBase = () => getOpenstackEndpoint('designate');
8182
export const masakariBase = () => getOpenstackEndpoint('masakari');
83+
export const blazarBase = () => getOpenstackEndpoint('blazar');
8284

8385
export const ironicOriginEndpoint = () => getOriginEndpoint('ironic');
8486
export const vpnEndpoint = () => getOriginEndpoint('neutron_vpn');
@@ -89,6 +91,7 @@ export const cinderEndpoint = () => getOriginEndpoint('cinder');
8991
export const manilaEndpoint = () => getOriginEndpoint('manilav2');
9092
export const zunEndpoint = () => getOriginEndpoint('zun');
9193
export const masakariEndpoint = () => getOriginEndpoint('masakari');
94+
export const blazarEndpoint = () => getOriginEndpoint('blazar');
9295
export const firewallEndpoint = () => getOriginEndpoint('neutron_firewall');
9396

9497
export const apiVersionMaps = {

src/client/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import zun from './zun';
3030
import magnum from './magnum';
3131
import masakari from './masakari';
3232
import designate from './designate';
33+
import blazar from './blazar';
3334

3435
const client = {
3536
skyline,
@@ -50,6 +51,7 @@ const client = {
5051
magnum,
5152
masakari,
5253
designate,
54+
blazar,
5355
};
5456

5557
window.client = client;

src/layouts/admin-menu.jsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
DeploymentUnitOutlined,
2828
BookOutlined,
2929
PlayCircleOutlined,
30+
CalendarOutlined,
3031
} from '@ant-design/icons';
3132

3233
const renderMenu = (t) => {
@@ -1098,6 +1099,36 @@ const renderMenu = (t) => {
10981099
},
10991100
],
11001101
},
1102+
{
1103+
path: '/reservation',
1104+
name: t('Reservation'),
1105+
key: 'reservationAdmin',
1106+
endpoints: 'blazar',
1107+
icon: <CalendarOutlined />,
1108+
children: [
1109+
{
1110+
path: '/reservation/lease',
1111+
name: t('Leases'),
1112+
key: 'leaseAdmin',
1113+
level: 1,
1114+
children: [
1115+
{
1116+
path: '/reservation/lease/create',
1117+
name: t('Create Lease'),
1118+
key: 'leaseCreateAdmin',
1119+
level: 2,
1120+
},
1121+
{
1122+
path: /^\/reservation\/lease\/detail\/.[^/]+$/,
1123+
name: t('Lease Detail'),
1124+
key: 'leaseDetailAdmin',
1125+
level: 2,
1126+
routePath: '/reservation/lease/detail/:id',
1127+
},
1128+
],
1129+
},
1130+
],
1131+
},
11011132
{
11021133
type: 'divider',
11031134
key: 'help-divider-admin',

src/layouts/menu.jsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
LockOutlined,
2727
BookOutlined,
2828
PlayCircleOutlined,
29+
CalendarOutlined,
2930
} from '@ant-design/icons';
3031

3132
const renderMenu = (t) => {
@@ -714,6 +715,36 @@ const renderMenu = (t) => {
714715
},
715716
],
716717
},
718+
{
719+
path: '/reservation',
720+
name: t('Reservation'),
721+
key: 'reservation',
722+
endpoints: 'blazar',
723+
icon: <CalendarOutlined />,
724+
children: [
725+
{
726+
path: '/reservation/lease',
727+
name: t('Leases'),
728+
key: 'lease',
729+
level: 1,
730+
children: [
731+
{
732+
path: '/reservation/lease/create',
733+
name: t('Create Lease'),
734+
key: 'leaseCreate',
735+
level: 2,
736+
},
737+
{
738+
path: /^\/reservation\/lease\/detail\/.[^/]+$/,
739+
name: t('Lease Detail'),
740+
key: 'leaseDetail',
741+
level: 2,
742+
routePath: '/reservation/lease/detail/:id',
743+
},
744+
],
745+
},
746+
],
747+
},
717748
{
718749
path: '/database',
719750
name: t('Database'),

src/pages/basic/routes/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ const E404 = lazy(() =>
6565
const InstanceHA = lazy(() =>
6666
import(/* webpackChunkName: "Inctance-HA" */ 'pages/ha/App')
6767
);
68+
const Reservation = lazy(() =>
69+
import(/* webpackChunkName: "reservation" */ 'pages/reservation/App')
70+
);
6871
const PATH = '/';
6972

7073
export default [
@@ -130,6 +133,10 @@ export default [
130133
path: `/ha`,
131134
component: InstanceHA,
132135
},
136+
{
137+
path: `/reservation`,
138+
component: Reservation,
139+
},
133140
{ path: '*', component: E404 },
134141
],
135142
},

src/pages/compute/containers/Instance/actions/StepCreate/BaseStep/index.jsx

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import globalImageStore from 'stores/glance/image';
2222
import globalInstanceSnapshotStore from 'stores/glance/instance-snapshot';
2323
import globalVolumeTypeStore from 'stores/cinder/volume-type';
2424
import globalAvailabilityZoneStore from 'stores/nova/zone';
25+
import globalLeaseStore from 'stores/blazar/lease';
2526
import { VolumeStore } from 'stores/cinder/volume';
2627
import {
2728
canImageCreateInstance,
@@ -34,6 +35,11 @@ import {
3435
import Base from 'components/Form';
3536
import InstanceVolume from 'components/FormItem/InstanceVolume';
3637
import { isGpuCategory } from 'resources/nova/flavor';
38+
import {
39+
isBlazarInternalAvailabilityZone,
40+
getUsableLeaseReservations,
41+
RESERVATION_TYPES,
42+
} from 'resources/blazar/reservation';
3743
import {
3844
volumeTypes,
3945
getDiskInfo,
@@ -48,12 +54,14 @@ export class BaseStep extends Base {
4854
this.volumeStore = new VolumeStore();
4955
this.volumeTypeStore = globalVolumeTypeStore;
5056
this.instanceSnapshotStore = globalInstanceSnapshotStore;
57+
this.leaseStore = globalLeaseStore;
5158
this.getAvailZones();
5259
this.getImages();
5360
this.getVolumeTypes();
5461
this.getVolumes();
5562
this.getInstanceSnapshots();
5663
this.initSourceChange();
64+
this.getHostReservations();
5765
}
5866

5967
get title() {
@@ -69,7 +77,7 @@ export class BaseStep extends Base {
6977
}
7078

7179
get defaultValue() {
72-
const { volume, snapshot } = this.locationParams;
80+
const { flavor, volume, snapshot } = this.locationParams;
7381
let source = this.imageSourceType;
7482
if (volume) {
7583
source = this.volumeSourceType;
@@ -85,12 +93,28 @@ export class BaseStep extends Base {
8593
if (source.value === 'image') {
8694
values.bootFromVolume = true;
8795
}
96+
if (flavor) {
97+
values.flavor = {
98+
selectedRowKeys: [flavor],
99+
selectedRows: [],
100+
};
101+
}
102+
const { reservation } = this.locationParams;
103+
if (reservation) {
104+
values.hostReservation = {
105+
selectedRowKeys: [reservation],
106+
selectedRows: this.hostReservations.filter(
107+
(it) => it.id === reservation
108+
),
109+
};
110+
}
88111
return values;
89112
}
90113

91114
get availableZones() {
92115
return (globalAvailabilityZoneStore.list.data || [])
93116
.filter((it) => it.zoneState.available)
117+
.filter((it) => !isBlazarInternalAvailabilityZone(it.zoneName))
94118
.map((it) => ({
95119
value: it.zoneName,
96120
label: it.zoneName,
@@ -210,6 +234,56 @@ export class BaseStep extends Base {
210234
}
211235
}
212236

237+
get enableBlazar() {
238+
return this.props.rootStore.checkEndpoint('blazar');
239+
}
240+
241+
get hostReservations() {
242+
if (!this.enableBlazar) {
243+
return [];
244+
}
245+
const leases = this.leaseStore.list.data || [];
246+
return leases.reduce((results, lease) => {
247+
const reservations = getUsableLeaseReservations(lease)
248+
.filter((r) => r.resource_type === RESERVATION_TYPES.HOST)
249+
.map((r) => ({ ...r, key: r.id, name: r.id }));
250+
return [...results, ...reservations];
251+
}, []);
252+
}
253+
254+
get hasHostReservationSelected() {
255+
// Only true when arriving from the lease detail page (?reservation=<id>).
256+
const { reservation } = this.locationParams;
257+
return !!reservation;
258+
}
259+
260+
get hostReservationFormItem() {
261+
const { reservation } = this.locationParams;
262+
263+
// Only show when arriving from the lease detail page (?reservation=<id>).
264+
// In the normal create instance flow there is no host reservation input.
265+
if (!reservation) {
266+
return { name: 'hostReservation', hidden: true };
267+
}
268+
269+
// Show as a read-only label — same pattern as the Project field.
270+
const displayText = reservation;
271+
return {
272+
name: 'hostReservation',
273+
label: t('Host Reservation'),
274+
type: 'label',
275+
content: displayText,
276+
};
277+
}
278+
279+
async getHostReservations() {
280+
if (!this.enableBlazar) {
281+
return;
282+
}
283+
await this.leaseStore.fetchList();
284+
this.updateDefaultValue();
285+
}
286+
213287
async getImages() {
214288
const { volume, image, snapshot } = this.locationParams;
215289
if (volume || snapshot) {
@@ -791,12 +865,14 @@ export class BaseStep extends Base {
791865
type: 'select',
792866
placeholder: t('Please select'),
793867
isWrappedValue: true,
794-
required: true,
868+
required: !this.hasHostReservationSelected,
869+
hidden: this.hasHostReservationSelected,
795870
options: this.availableZones,
796871
tip: t(
797872
'Availability zone refers to a physical area where power and network are independent of each other in the same area. In the same region, the availability zone and the availability zone can communicate with each other in the intranet, and the available zones can achieve fault isolation.'
798873
),
799874
},
875+
this.hostReservationFormItem,
800876
{
801877
type: 'divider',
802878
},

src/pages/compute/containers/Instance/actions/StepCreate/ConfirmStep/index.jsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,16 @@ export class ConfirmStep extends Base {
195195
return serverGroup.selectedRows[0].name;
196196
}
197197

198+
getHostReservation() {
199+
const { context } = this.props;
200+
const { hostReservation } = context;
201+
const { selectedRowKeys = [] } = hostReservation || {};
202+
if (!selectedRowKeys.length) {
203+
return null;
204+
}
205+
return selectedRowKeys[0];
206+
}
207+
198208
getIso() {
199209
const { context } = this.props;
200210
const { iso } = context;
@@ -214,6 +224,12 @@ export class ConfirmStep extends Base {
214224
return {};
215225
}
216226

227+
getAvailableZone() {
228+
const { context } = this.props;
229+
const { availableZone } = context;
230+
return (availableZone && availableZone.label) || '-';
231+
}
232+
217233
get formItems() {
218234
const { context } = this.props;
219235
let baseItems = [
@@ -236,7 +252,7 @@ export class ConfirmStep extends Base {
236252
},
237253
{
238254
label: t('Available Zone'),
239-
value: context.availableZone.label,
255+
value: this.getAvailableZone(),
240256
},
241257
{
242258
label: t('Start Source Name'),
@@ -255,6 +271,14 @@ export class ConfirmStep extends Base {
255271
label: t('Flavor'),
256272
value: this.getFlavor(),
257273
},
274+
...(this.getHostReservation()
275+
? [
276+
{
277+
label: t('Host Reservation'),
278+
value: this.getHostReservation(),
279+
},
280+
]
281+
: []),
258282
];
259283
if (!this.enableCinder) {
260284
baseItems = baseItems.filter(

0 commit comments

Comments
 (0)