1use boulder::{BuildableWithPersianRug, GeneratableWithPersianRug};
2use boulder::{Inc, Pattern, Some as GSome};
3use django_query::{
4 filtering::FilterableWithPersianRug, row::IntoRowWithPersianRug,
5 sorting::SortableWithPersianRug,
6};
7use persian_rug::{contextual, Context, Proxy};
8use strum::{Display, EnumString};
9
10use crate::{
11 Alias, Architecture, BitWidth, Core, DeviceType, Group, Job, ProcessorFamily, Tag, User, Worker,
12};
13
14#[derive(
16 Clone,
17 Debug,
18 FilterableWithPersianRug,
19 SortableWithPersianRug,
20 IntoRowWithPersianRug,
21 BuildableWithPersianRug,
22 GeneratableWithPersianRug,
23)]
24#[boulder(
25 persian_rug(
26 context=C,
27 access(
28 Device<C>,
29 DeviceType<C>,
30 Alias<C>,
31 Architecture<C>,
32 BitWidth<C>,
33 Core<C>,
34 ProcessorFamily<C>,
35 User<C>,
36 Group<C>,
37 Worker<C>,
38 Job<C>,
39 Tag<C>
40 )
41 )
42)]
43#[django(
44 persian_rug(
45 context=C,
46 access(
47 Device<C>,
48 DeviceType<C>,
49 Alias<C>,
50 Architecture<C>,
51 BitWidth<C>,
52 Core<C>,
53 ProcessorFamily<C>,
54 User<C>,
55 Group<C>,
56 Worker<C>,
57 Job<C>,
58 Tag<C>
59 )
60 )
61)]
62#[contextual(C)]
63pub struct Device<C: Context + 'static> {
64 #[boulder(default="test-device",
65 generator=Pattern!("test-device-{}", Inc(0)))]
66 #[django(sort, op(in, contains, icontains, startswith, endswith))]
67 pub hostname: String,
68 #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
69 #[django(sort("name"), traverse, foreign_key = "name")]
70 pub device_type: Proxy<DeviceType<C>>,
71 #[boulder(default=Some("1.0".to_string()))]
72 #[django(sort, op(in, contains, icontains, startswith, endswith))]
73 pub device_version: Option<String>,
74 #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
75 #[django(sort("id"), traverse, foreign_key = "id")]
76 pub physical_owner: Option<Proxy<User<C>>>,
77 #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
78 #[django(sort("id"), traverse, foreign_key = "id")]
79 pub physical_group: Option<Proxy<Group<C>>>,
80 #[boulder(default=Some("Test description for device.".to_string()),
81 generator=GSome(Pattern!("Test description {}", Inc(0))))]
82 #[django(sort, op(in, contains, icontains, startswith, endswith))]
83 pub description: Option<String>,
84
85 #[boulder(generatable_with_persian_rug, sequence = 3usize)]
90 #[django(traverse, foreign_key = "id")]
91 pub tags: Vec<Proxy<Tag<C>>>,
92
93 #[django(sort)]
94 #[boulder(default=State::Idle)]
95 pub state: State,
96 #[django(sort)]
97 #[boulder(default=Health::Good)]
98 pub health: Health,
99 #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
100 #[django(sort("hostname"), traverse, foreign_key = "hostname")]
101 pub worker_host: Proxy<Worker<C>>,
102
103 #[django(unfilterable)]
104 pub is_synced: bool,
105 #[django(unfilterable, foreign_key = "id")]
106 pub last_health_report_job: Option<Proxy<Job<C>>>,
107}
108
109#[derive(Clone, Debug, PartialEq, Eq, EnumString, PartialOrd, Ord, Display)]
111pub enum Health {
112 Unknown,
113 Maintenance,
114 Good,
115 Bad,
116 Looping,
117 Retired,
118}
119
120impl django_query::filtering::ops::Scalar for Health {}
121impl django_query::row::StringCellValue for Health {}
122
123#[derive(Clone, Debug, PartialEq, Eq, EnumString, PartialOrd, Ord, Display)]
125pub enum State {
126 Idle,
127 Reserved,
128 Running,
129}
130
131impl django_query::filtering::ops::Scalar for State {}
132impl django_query::row::StringCellValue for State {}
133
134#[cfg(test)]
135mod test {
136 use super::*;
137 use crate::state::{self, SharedState};
138
139 use anyhow::Result;
140 use boulder::BuilderWithPersianRug;
141 use boulder::GeneratorWithPersianRugIterator;
142 use boulder::Repeat;
143 use serde_json::{json, Value};
144 use test_log::test;
145
146 async fn make_request<T, U>(server_uri: T, endpoint: U) -> Result<Value>
147 where
148 T: AsRef<str>,
149 U: AsRef<str>,
150 {
151 let url = format!("{}/api/v0.2/{}", server_uri.as_ref(), endpoint.as_ref());
152 Ok(reqwest::get(&url).await?.json().await?)
153 }
154
155 #[tokio::test]
156 async fn test_devices() {
157 let mut p = SharedState::new();
158 {
159 let m = p.mutate();
160
161 let (worker, m) = Proxy::<Worker<_>>::builder().hostname("worker1").build(m);
162 let (device_type, m) = Proxy::<DeviceType<_>>::builder().name("type1").build(m);
163 let (_, m) = Proxy::<Device<_>>::builder()
164 .hostname("test1")
165 .worker_host(worker)
166 .device_type(device_type)
167 .description(Some("description of device".to_string()))
168 .health(Health::Good)
169 .build(m);
170
171 let (worker, m) = Proxy::<Worker<_>>::builder().hostname("worker2").build(m);
172 let (device_type, m) = Proxy::<DeviceType<_>>::builder().name("type2").build(m);
173 let _ = Proxy::<Device<state::State>>::builder()
174 .hostname("test2")
175 .worker_host(worker)
176 .device_type(device_type)
177 .description(Some("description of device".to_string()))
178 .health(Health::Bad)
179 .build(m);
180 }
181
182 let server = wiremock::MockServer::start().await;
183
184 wiremock::Mock::given(wiremock::matchers::method("GET"))
185 .and(wiremock::matchers::path("/api/v0.2/devices/"))
186 .respond_with(p.endpoint::<Device<state::State>>(Some(&server.uri()), None))
187 .mount(&server)
188 .await;
189
190 let devices = make_request(server.uri(), "devices/")
191 .await
192 .expect("failed to query devices");
193
194 assert_eq!(devices["results"][0]["hostname"], json!("test1"));
195 assert_eq!(devices["results"][1]["hostname"], json!("test2"));
196 assert_eq!(devices["results"].as_array().unwrap().len(), 2);
197 }
198
199 #[tokio::test]
200 async fn test_device_builder() {
201 let mut p = SharedState::new();
202 {
203 let m = p.mutate();
204 let (worker, m) = Proxy::<Worker<_>>::builder().hostname("worker1").build(m);
205 let (_, m) = Proxy::<Device<state::State>>::builder()
206 .hostname("test1")
207 .worker_host(worker)
208 .build(m);
209 let (worker, m) = Proxy::<Worker<_>>::builder().hostname("worker2").build(m);
210 let _ = Proxy::<Device<state::State>>::builder()
211 .hostname("test2")
212 .worker_host(worker)
213 .build(m);
214 }
215
216 let server = wiremock::MockServer::start().await;
217
218 wiremock::Mock::given(wiremock::matchers::method("GET"))
219 .and(wiremock::matchers::path("/api/v0.2/devices/"))
220 .respond_with(p.endpoint::<Device<state::State>>(Some(&server.uri()), None))
221 .mount(&server)
222 .await;
223
224 let devices = make_request(server.uri(), "devices/")
225 .await
226 .expect("failed to query devices");
227
228 assert_eq!(devices["results"][0]["hostname"], json!("test1"));
229 assert_eq!(devices["results"][1]["hostname"], json!("test2"));
230 assert_eq!(devices["results"].as_array().unwrap().len(), 2);
231 }
232
233 #[tokio::test]
234 async fn test_device_stream() {
235 let mut p = SharedState::new();
236 {
237 let m = p.mutate();
238 let devices = Proxy::<Device<_>>::generator().hostname(Repeat!("w1", "w2"));
239 let _ = GeneratorWithPersianRugIterator::new(devices, m)
240 .take(2)
241 .collect::<Vec<_>>();
242 }
243
244 let server = wiremock::MockServer::start().await;
245
246 wiremock::Mock::given(wiremock::matchers::method("GET"))
247 .and(wiremock::matchers::path("/api/v0.2/devices/"))
248 .respond_with(p.endpoint::<Device<state::State>>(Some(&server.uri()), None))
249 .mount(&server)
250 .await;
251
252 let devices = make_request(server.uri(), "devices/")
253 .await
254 .expect("failed to query devices");
255
256 assert_eq!(devices["results"][0]["hostname"], json!("w1"));
257 assert_eq!(devices["results"][1]["hostname"], json!("w2"));
258 assert_eq!(devices["results"].as_array().unwrap().len(), 2);
259 }
260
261 #[test(tokio::test)]
262 async fn test_output() {
263 let mut p = SharedState::new();
264 {
265 let m = p.mutate();
266 let gen = Proxy::<Device<_>>::generator()
269 .health(Repeat!(Health::Maintenance, Health::Good))
270 .description(|| None)
271 .device_version(|| None)
272 .physical_owner(|| None)
273 .physical_group(|| None)
274 .last_health_report_job(|| None); let _ = GeneratorWithPersianRugIterator::new(gen, m)
278 .take(4)
279 .collect::<Vec<_>>();
280 }
281
282 let server = wiremock::MockServer::start().await;
283 let ep = p.endpoint::<Device<_>>(Some(&server.uri()), Some(2));
284
285 wiremock::Mock::given(wiremock::matchers::method("GET"))
286 .and(wiremock::matchers::path("/api/v0.2/devices/"))
287 .respond_with(ep)
288 .mount(&server)
289 .await;
290
291 let body: serde_json::Value =
292 reqwest::get(&format!("{}/api/v0.2/devices/?limit=2", server.uri()))
293 .await
294 .expect("error getting devices")
295 .json()
296 .await
297 .expect("error parsing devices");
298
299 let next = format!("{}/api/v0.2/devices/?limit=2&offset=2", server.uri());
300
301 assert_eq!(
302 body,
303 serde_json::json! {
304 {
305 "count": 4,
306 "next": next,
307 "previous": null,
308 "results": [
309 {
310 "hostname": "test-device-0",
311 "device_type": "test-device-type-0",
312 "device_version": null,
313 "physical_owner": null,
314 "physical_group": null,
315 "description": null,
316 "tags": [
317 0,
318 1,
319 2
320 ],
321 "state": "Idle",
322 "health": "Maintenance",
323 "last_health_report_job": null,
324 "worker_host": "a-test-worker-1",
325 "is_synced": false
326 },
327 {
328 "hostname": "test-device-1",
329 "device_type": "test-device-type-1",
330 "device_version": null,
331 "physical_owner": null,
332 "physical_group": null,
333 "description": null,
334 "tags": [
335 3,
336 4,
337 5
338 ],
339 "state": "Idle",
340 "health": "Good",
341 "last_health_report_job": null,
342 "worker_host": "a-test-worker-2",
343 "is_synced": false
344 },
345 ]
346 }
347 }
348 );
349 }
350}